1use super::{
4 DeterministicTextMeasurer, FLOWCHART_DEFAULT_FONT_KEY, TextMeasurer, TextMetrics, TextStyle,
5 WrapMode, flowchart_default_bold_delta_em, flowchart_default_bold_kern_delta_em,
6 flowchart_default_bold_svg_right_overhang_em, font_key_uses_courier_metrics,
7 is_flowchart_default_font, overrides, round_to_1_64_px, style_requests_bold_font_weight,
8 svg_wrapped_first_line_bbox_height_px,
9};
10
11#[derive(Debug, Clone, Default)]
12pub struct VendoredFontMetricsTextMeasurer {
13 fallback: DeterministicTextMeasurer,
14}
15
16#[derive(Clone, Copy)]
17struct FontMetricProfile<'a> {
18 entries: &'a [(char, f64)],
19 default_em: f64,
20 kern_pairs: &'a [(u32, u32, f64)],
21 space_trigrams: &'a [(u32, u32, f64)],
22 trigrams: &'a [(u32, u32, u32, f64)],
23 missing_v_comma_kern_em: f64,
24 missing_t_o_kern_em: f64,
25 missing_t_r_kern_em: f64,
26 missing_space_before_capital_a_em: f64,
27 missing_space_after_capital_a_before_open_paren_em: f64,
28}
29
30impl VendoredFontMetricsTextMeasurer {
31 fn metric_profile(
32 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
33 ) -> FontMetricProfile<'_> {
34 FontMetricProfile {
35 entries: table.entries,
36 default_em: table.default_em.max(0.1),
37 kern_pairs: table.kern_pairs,
38 space_trigrams: table.space_trigrams,
39 trigrams: table.trigrams,
40 missing_v_comma_kern_em: if table.font_key == FLOWCHART_DEFAULT_FONT_KEY {
41 -140.0 / 1024.0
42 } else {
43 0.0
44 },
45 missing_t_o_kern_em: if table.font_key == FLOWCHART_DEFAULT_FONT_KEY {
46 -128.0 / 1024.0
47 } else {
48 0.0
49 },
50 missing_t_r_kern_em: if table.font_key == FLOWCHART_DEFAULT_FONT_KEY {
51 -113.0 / 1024.0
52 } else {
53 0.0
54 },
55 missing_space_before_capital_a_em: if table.font_key
56 == "trebuchetms,verdana,arial,sans-serif"
57 {
58 -57.0 / 1024.0
59 } else {
60 0.0
61 },
62 missing_space_after_capital_a_before_open_paren_em: if table.font_key
63 == "trebuchetms,verdana,arial,sans-serif"
64 {
65 -57.0 / 1024.0
66 } else {
67 0.0
68 },
69 }
70 }
71
72 pub(super) fn quantize_svg_bbox_px_nearest(v: f64) -> f64 {
73 if !(v.is_finite() && v >= 0.0) {
74 return 0.0;
75 }
76 let x = v * 1024.0;
80 let f = x.floor();
81 let frac = x - f;
82 let i = if frac < 0.5 {
83 f
84 } else if frac > 0.5 {
85 f + 1.0
86 } else {
87 let fi = f as i64;
88 if fi % 2 == 0 { f } else { f + 1.0 }
89 };
90 i / 1024.0
91 }
92
93 fn quantize_svg_half_px_nearest(half_px: f64) -> f64 {
94 if !(half_px.is_finite() && half_px >= 0.0) {
95 return 0.0;
96 }
97 (half_px * 256.0).floor() / 256.0
101 }
102
103 fn normalize_font_key(s: &str) -> String {
104 s.chars()
105 .filter_map(|ch| {
106 if ch.is_whitespace() || ch == '"' || ch == '\'' || ch == ';' {
110 None
111 } else {
112 Some(ch.to_ascii_lowercase())
113 }
114 })
115 .collect()
116 }
117
118 fn lookup_table(
119 &self,
120 style: &TextStyle,
121 ) -> Option<&'static crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables> {
122 let key = style
123 .font_family
124 .as_deref()
125 .map(Self::normalize_font_key)
126 .unwrap_or_default();
127 let key = if key.is_empty() {
128 FLOWCHART_DEFAULT_FONT_KEY
131 } else {
132 key.as_str()
133 };
134 if let Some(t) = crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(key)
135 {
136 return Some(t);
137 }
138
139 let key_lower = key;
142 if font_key_uses_courier_metrics(key_lower) {
143 return crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(
144 "courier",
145 );
146 }
147 if key_lower.contains("sans-serif") {
151 return crate::generated::font_metrics_flowchart_11_12_2::lookup_font_metrics(
152 "sans-serif",
153 );
154 }
155 None
156 }
157
158 fn lookup_char_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
159 fn find_entry_em(entries: &[(char, f64)], ch: char) -> Option<f64> {
160 let mut lo = 0usize;
161 let mut hi = entries.len();
162 while lo < hi {
163 let mid = (lo + hi) / 2;
164 match entries[mid].0.cmp(&ch) {
165 std::cmp::Ordering::Equal => return Some(entries[mid].1),
166 std::cmp::Ordering::Less => lo = mid + 1,
167 std::cmp::Ordering::Greater => hi = mid,
168 }
169 }
170 None
171 }
172
173 if let Some(em) = find_entry_em(entries, ch) {
174 return em;
175 }
176
177 let paired = match ch {
182 '(' => Some(')'),
183 ')' => Some('('),
184 '[' => Some(']'),
185 ']' => Some('['),
186 '{' => Some('}'),
187 '}' => Some('{'),
188 _ => None,
189 };
190 if let Some(other) = paired {
191 if let Some(other_em) = find_entry_em(entries, other) {
192 return other_em;
193 }
194 }
195 if ch.is_ascii() {
196 return default_em;
197 }
198
199 if ('\u{80}'..='\u{9f}').contains(&ch) {
200 return 0.598_7;
205 }
206
207 Self::lookup_non_ascii_fallback_em(default_em, ch)
208 }
209
210 fn lookup_non_ascii_fallback_em(default_em: f64, ch: char) -> f64 {
211 let code = ch as u32;
212
213 if unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) == 0
219 || (0x1f3fb..=0x1f3ff).contains(&code)
220 {
221 return 0.0;
222 }
223 if (0x0590..=0x05ff).contains(&code) {
224 return 0.479_980_468_75;
225 }
226 if (0x1f300..=0x1faff).contains(&code) || (0x2600..=0x27bf).contains(&code) {
227 return 1.249_67;
228 }
229 if (0xac00..=0xd7af).contains(&code) {
230 return 0.864_257_812_5;
231 }
232
233 match unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1) {
234 2.. => 1.0,
235 _ => default_em,
236 }
237 }
238
239 fn lookup_kern_em(kern_pairs: &[(u32, u32, f64)], a: char, b: char) -> f64 {
240 let key_a = a as u32;
241 let key_b = b as u32;
242 let mut lo = 0usize;
243 let mut hi = kern_pairs.len();
244 while lo < hi {
245 let mid = (lo + hi) / 2;
246 let (ma, mb, v) = kern_pairs[mid];
247 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
248 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
249 (std::cmp::Ordering::Less, _) => lo = mid + 1,
250 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
251 _ => hi = mid,
252 }
253 }
254 0.0
255 }
256
257 fn lookup_profile_kern_em(profile: FontMetricProfile<'_>, a: char, b: char) -> f64 {
258 let explicit = Self::lookup_kern_em(profile.kern_pairs, a, b);
259 if explicit != 0.0 {
260 return explicit;
261 }
262
263 if a == 'v' && b == ',' {
264 return profile.missing_v_comma_kern_em;
269 }
270 if a == 'T' && b == 'o' {
271 return profile.missing_t_o_kern_em;
272 }
273 if a == 'T' && b == 'r' {
274 return profile.missing_t_r_kern_em;
275 }
276
277 0.0
278 }
279
280 fn lookup_space_trigram_em(space_trigrams: &[(u32, u32, f64)], a: char, b: char) -> f64 {
281 let key_a = a as u32;
282 let key_b = b as u32;
283 let mut lo = 0usize;
284 let mut hi = space_trigrams.len();
285 while lo < hi {
286 let mid = (lo + hi) / 2;
287 let (ma, mb, v) = space_trigrams[mid];
288 match (ma.cmp(&key_a), mb.cmp(&key_b)) {
289 (std::cmp::Ordering::Equal, std::cmp::Ordering::Equal) => return v,
290 (std::cmp::Ordering::Less, _) => lo = mid + 1,
291 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less) => lo = mid + 1,
292 _ => hi = mid,
293 }
294 }
295 0.0
296 }
297
298 fn lookup_trigram_em(trigrams: &[(u32, u32, u32, f64)], a: char, b: char, c: char) -> f64 {
299 let key_a = a as u32;
300 let key_b = b as u32;
301 let key_c = c as u32;
302 let mut lo = 0usize;
303 let mut hi = trigrams.len();
304 while lo < hi {
305 let mid = (lo + hi) / 2;
306 let (ma, mb, mc, v) = trigrams[mid];
307 match (ma.cmp(&key_a), mb.cmp(&key_b), mc.cmp(&key_c)) {
308 (
309 std::cmp::Ordering::Equal,
310 std::cmp::Ordering::Equal,
311 std::cmp::Ordering::Equal,
312 ) => return v,
313 (std::cmp::Ordering::Less, _, _) => lo = mid + 1,
314 (std::cmp::Ordering::Equal, std::cmp::Ordering::Less, _) => lo = mid + 1,
315 (
316 std::cmp::Ordering::Equal,
317 std::cmp::Ordering::Equal,
318 std::cmp::Ordering::Less,
319 ) => lo = mid + 1,
320 _ => hi = mid,
321 }
322 }
323 0.0
324 }
325
326 fn is_tiny_lattice_residual_em(v: f64) -> bool {
327 v.abs() <= (1.0 / 1024.0) + 1e-12
332 }
333
334 fn same_glyph_pair_kern_em(
335 profile: FontMetricProfile<'_>,
336 a: char,
337 b: char,
338 same_run_len_after: usize,
339 ) -> f64 {
340 let kern = Self::lookup_profile_kern_em(profile, a, b);
341 if a == b && Self::is_tiny_lattice_residual_em(kern) && same_run_len_after % 2 == 1 {
342 0.0
343 } else {
344 kern
345 }
346 }
347
348 fn same_glyph_trigram_em(profile: FontMetricProfile<'_>, a: char, b: char, c: char) -> f64 {
349 let delta = Self::lookup_trigram_em(profile.trigrams, a, b, c);
350 if a == b && b == c && Self::is_tiny_lattice_residual_em(delta) {
351 0.0
352 } else {
353 delta
354 }
355 }
356
357 fn lookup_html_override_em(overrides: &[(&'static str, f64)], text: &str) -> Option<f64> {
358 let mut lo = 0usize;
359 let mut hi = overrides.len();
360 while lo < hi {
361 let mid = (lo + hi) / 2;
362 let (k, v) = overrides[mid];
363 match k.cmp(text) {
364 std::cmp::Ordering::Equal => return Some(v),
365 std::cmp::Ordering::Less => lo = mid + 1,
366 std::cmp::Ordering::Greater => hi = mid,
367 }
368 }
369 None
370 }
371
372 fn lookup_svg_override_em(
373 overrides: &[(&'static str, f64, f64)],
374 text: &str,
375 ) -> Option<(f64, f64)> {
376 let mut lo = 0usize;
377 let mut hi = overrides.len();
378 while lo < hi {
379 let mid = (lo + hi) / 2;
380 let (k, l, r) = overrides[mid];
381 match k.cmp(text) {
382 std::cmp::Ordering::Equal => return Some((l, r)),
383 std::cmp::Ordering::Less => lo = mid + 1,
384 std::cmp::Ordering::Greater => hi = mid,
385 }
386 }
387 None
388 }
389
390 fn lookup_overhang_em(entries: &[(char, f64)], default_em: f64, ch: char) -> f64 {
391 let mut lo = 0usize;
392 let mut hi = entries.len();
393 while lo < hi {
394 let mid = (lo + hi) / 2;
395 match entries[mid].0.cmp(&ch) {
396 std::cmp::Ordering::Equal => return entries[mid].1,
397 std::cmp::Ordering::Less => lo = mid + 1,
398 std::cmp::Ordering::Greater => hi = mid,
399 }
400 }
401 default_em
402 }
403
404 fn line_svg_bbox_extents_px(
405 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
406 text: &str,
407 font_size: f64,
408 ) -> (f64, f64) {
409 let profile = Self::metric_profile(table);
410 let t = text.trim_end();
411 if t.is_empty() {
412 return (0.0, 0.0);
413 }
414
415 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
416 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
417 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
418 return (left, right);
419 }
420
421 if let Some((left, right)) =
422 overrides::lookup_flowchart_svg_bbox_x_px(table.font_key, font_size, t)
423 {
424 return (left, right);
425 }
426
427 let first = t.chars().next().unwrap_or(' ');
428 let last = t.chars().last().unwrap_or(' ');
429
430 let advance_px_unscaled = {
437 let words: Vec<&str> = t.split_whitespace().filter(|s| !s.is_empty()).collect();
438 if words.len() >= 2 {
439 let mut sum_px = 0.0f64;
440 for (idx, w) in words.iter().enumerate() {
441 if idx == 0 {
442 sum_px += Self::line_width_px(profile, w, false, font_size);
443 } else {
444 let seg = format!(" {w}");
445 sum_px += Self::line_width_px(profile, &seg, false, font_size);
446 }
447 }
448 sum_px
449 } else {
450 Self::line_width_px(profile, t, false, font_size)
451 }
452 };
453
454 let advance_px = advance_px_unscaled * table.svg_scale;
455 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
456 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
464 0.0
465 } else {
466 Self::lookup_overhang_em(
467 table.svg_bbox_overhang_left,
468 table.svg_bbox_overhang_left_default_em,
469 first,
470 )
471 };
472 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
473 0.0
474 } else {
475 Self::lookup_overhang_em(
476 table.svg_bbox_overhang_right,
477 table.svg_bbox_overhang_right_default_em,
478 last,
479 )
480 };
481
482 let left = (half + left_oh_em * font_size).max(0.0);
483 let right = (half + right_oh_em * font_size).max(0.0);
484 (left, right)
485 }
486
487 fn line_svg_bbox_extents_px_single_run(
488 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
489 text: &str,
490 font_size: f64,
491 ) -> (f64, f64) {
492 let profile = Self::metric_profile(table);
493 let t = text.trim_end();
494 if t.is_empty() {
495 return (0.0, 0.0);
496 }
497
498 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
499 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
500 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
501 return (left, right);
502 }
503
504 let first = t.chars().next().unwrap_or(' ');
505 let last = t.chars().last().unwrap_or(' ');
506
507 let advance_px_unscaled = Self::line_width_px(profile, t, false, font_size);
510
511 let advance_px = advance_px_unscaled * table.svg_scale;
512 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
513
514 let left_oh_em = if first.is_ascii() && !matches!(first, '[' | '(' | '{') {
515 0.0
516 } else {
517 Self::lookup_overhang_em(
518 table.svg_bbox_overhang_left,
519 table.svg_bbox_overhang_left_default_em,
520 first,
521 )
522 };
523 let right_oh_em = if last.is_ascii() && !matches!(last, ']' | ')' | '}') {
524 0.0
525 } else {
526 Self::lookup_overhang_em(
527 table.svg_bbox_overhang_right,
528 table.svg_bbox_overhang_right_default_em,
529 last,
530 )
531 };
532
533 let left = (half + left_oh_em * font_size).max(0.0);
534 let right = (half + right_oh_em * font_size).max(0.0);
535 (left, right)
536 }
537
538 fn line_svg_bbox_extents_px_single_run_with_ascii_overhang(
539 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
540 text: &str,
541 font_size: f64,
542 ) -> (f64, f64) {
543 Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang_and_weight(
544 table, text, font_size, false,
545 )
546 }
547
548 fn line_svg_bbox_extents_px_single_run_with_ascii_overhang_and_weight(
549 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
550 text: &str,
551 font_size: f64,
552 bold: bool,
553 ) -> (f64, f64) {
554 let profile = Self::metric_profile(table);
555 let t = text.trim_end();
556 if t.is_empty() {
557 return (0.0, 0.0);
558 }
559
560 if let Some((left_em, right_em)) = Self::lookup_svg_override_em(table.svg_overrides, t) {
561 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
562 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
563 return (left, right);
564 }
565
566 let first = t.chars().next().unwrap_or(' ');
567 let last = t.chars().last().unwrap_or(' ');
568
569 let advance_px_unscaled = Self::line_width_px(profile, t, bold, font_size);
570
571 let advance_px = advance_px_unscaled * table.svg_scale;
572 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
573
574 let left_oh_em = Self::lookup_overhang_em(
575 table.svg_bbox_overhang_left,
576 table.svg_bbox_overhang_left_default_em,
577 first,
578 );
579 let mut right_oh_em = Self::lookup_overhang_em(
580 table.svg_bbox_overhang_right,
581 table.svg_bbox_overhang_right_default_em,
582 last,
583 );
584 if bold && table.font_key == FLOWCHART_DEFAULT_FONT_KEY {
585 right_oh_em = right_oh_em.max(flowchart_default_bold_svg_right_overhang_em(last));
586 }
587
588 let left = (half + left_oh_em * font_size).max(0.0);
589 let right = (half + right_oh_em * font_size).max(0.0);
590 (left, right)
591 }
592
593 fn line_svg_bbox_width_px(
594 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
595 text: &str,
596 font_size: f64,
597 ) -> f64 {
598 let (l, r) = Self::line_svg_bbox_extents_px(table, text, font_size);
599 (l + r).max(0.0)
600 }
601
602 fn line_svg_bbox_width_single_run_px(
603 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
604 text: &str,
605 font_size: f64,
606 ) -> f64 {
607 let t = text.trim_end();
608 if !t.is_empty() {
609 if let Some((left_em, right_em)) =
610 overrides::lookup_sequence_svg_override_em(table.font_key, t)
611 {
612 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
613 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
614 return (left + right).max(0.0);
615 }
616 }
617
618 let (l, r) = Self::line_svg_bbox_extents_px_single_run(table, text, font_size);
619 (l + r).max(0.0)
620 }
621
622 fn line_svg_title_bbox_extents_px(
623 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
624 text: &str,
625 font_size: f64,
626 ) -> (f64, f64) {
627 let profile = Self::metric_profile(table);
628 let t = text.trim_end();
629 if t.is_empty() {
630 return (0.0, 0.0);
631 }
632
633 let advance_px = if let Some(em) = Self::lookup_html_override_em(table.html_overrides, t) {
638 em * font_size
639 } else {
640 Self::line_width_px(profile, t, false, font_size) * table.svg_scale
641 };
642 let half = Self::quantize_svg_half_px_nearest((advance_px / 2.0).max(0.0));
643 (half, half)
644 }
645
646 fn split_token_to_svg_bbox_width_px(
647 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
648 tok: &str,
649 max_width_px: f64,
650 font_size: f64,
651 ) -> (String, String) {
652 if max_width_px <= 0.0 {
653 return (tok.to_string(), String::new());
654 }
655 let chars = tok.chars().collect::<Vec<_>>();
656 if chars.is_empty() {
657 return (String::new(), String::new());
658 }
659
660 let first = chars[0];
661 let left_oh_em = if first.is_ascii() {
662 0.0
663 } else {
664 Self::lookup_overhang_em(
665 table.svg_bbox_overhang_left,
666 table.svg_bbox_overhang_left_default_em,
667 first,
668 )
669 };
670
671 let mut em = 0.0;
672 let mut prev: Option<char> = None;
673 let mut split_at = 1usize;
674 for (idx, ch) in chars.iter().enumerate() {
675 em += Self::lookup_char_em(table.entries, table.default_em.max(0.1), *ch);
676 if let Some(p) = prev {
677 em += Self::lookup_kern_em(table.kern_pairs, p, *ch);
678 }
679 prev = Some(*ch);
680
681 let right_oh_em = if ch.is_ascii() {
682 0.0
683 } else {
684 Self::lookup_overhang_em(
685 table.svg_bbox_overhang_right,
686 table.svg_bbox_overhang_right_default_em,
687 *ch,
688 )
689 };
690 let half_px = Self::quantize_svg_half_px_nearest(
691 (em * font_size * table.svg_scale / 2.0).max(0.0),
692 );
693 let w_px = 2.0 * half_px + (left_oh_em + right_oh_em) * font_size;
694 if w_px.is_finite() && w_px <= max_width_px {
695 split_at = idx + 1;
696 } else if idx > 0 {
697 break;
698 }
699 }
700 let head = chars[..split_at].iter().collect::<String>();
701 let tail = chars[split_at..].iter().collect::<String>();
702 (head, tail)
703 }
704
705 fn wrap_text_lines_svg_bbox_px(
706 table: &crate::generated::font_metrics_flowchart_11_12_2::FontMetricsTables,
707 text: &str,
708 max_width_px: Option<f64>,
709 font_size: f64,
710 tokenize_whitespace: bool,
711 ) -> Vec<String> {
712 const EPS_PX: f64 = 0.125;
713 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
714 let width_fn = if tokenize_whitespace {
715 Self::line_svg_bbox_width_px
716 } else {
717 Self::line_svg_bbox_width_single_run_px
718 };
719
720 let mut lines = Vec::new();
721 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
722 let Some(w) = max_width_px else {
723 lines.push(line);
724 continue;
725 };
726
727 let mut tokens = std::collections::VecDeque::from(
728 DeterministicTextMeasurer::split_line_to_words(&line),
729 );
730 let mut out: Vec<String> = Vec::new();
731 let mut cur = String::new();
732
733 while let Some(tok) = tokens.pop_front() {
734 if cur.is_empty() && tok == " " {
735 continue;
736 }
737
738 let candidate = format!("{cur}{tok}");
739 let candidate_trimmed = candidate.trim_end();
740 if width_fn(table, candidate_trimmed, font_size) <= w + EPS_PX {
741 cur = candidate;
742 continue;
743 }
744
745 if !cur.trim().is_empty() {
746 out.push(cur.trim_end().to_string());
747 cur.clear();
748 tokens.push_front(tok);
749 continue;
750 }
751
752 if tok == " " {
753 continue;
754 }
755
756 if width_fn(table, tok.as_str(), font_size) <= w + EPS_PX {
757 cur = tok;
758 continue;
759 }
760
761 let (head, tail) =
763 Self::split_token_to_svg_bbox_width_px(table, &tok, w + EPS_PX, font_size);
764 out.push(head);
765 if !tail.is_empty() {
766 tokens.push_front(tail);
767 }
768 }
769
770 if !cur.trim().is_empty() {
771 out.push(cur.trim_end().to_string());
772 }
773
774 if out.is_empty() {
775 lines.push("".to_string());
776 } else {
777 lines.extend(out);
778 }
779 }
780
781 if lines.is_empty() {
782 vec!["".to_string()]
783 } else {
784 lines
785 }
786 }
787
788 fn line_width_px(
789 profile: FontMetricProfile<'_>,
790 text: &str,
791 bold: bool,
792 font_size: f64,
793 ) -> f64 {
794 fn normalize_whitespace_like(ch: char) -> (char, f64) {
795 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
803 if ch == '\u{00A0}' {
804 (' ', NBSP_DELTA_EM)
805 } else {
806 (ch, 0.0)
807 }
808 }
809
810 let mut em = 0.0;
811 let mut prevprev: Option<char> = None;
812 let mut prev: Option<char> = None;
813 let mut same_run_len = 0usize;
814 for ch in text.chars() {
815 let (ch, delta_em) = normalize_whitespace_like(ch);
816 let next_same_run_len = if prev == Some(ch) {
817 same_run_len + 1
818 } else {
819 1
820 };
821 em += Self::lookup_char_em(profile.entries, profile.default_em, ch) + delta_em;
822 if let Some(p) = prev {
823 em += Self::same_glyph_pair_kern_em(profile, p, ch, next_same_run_len);
824 }
825 if bold {
826 if let Some(p) = prev {
827 em += flowchart_default_bold_kern_delta_em(p, ch);
828 }
829 em += flowchart_default_bold_delta_em(ch);
830 }
831 if let (Some(a), Some(b)) = (prevprev, prev) {
832 if b == ' ' {
833 if !(a.is_whitespace() || ch.is_whitespace()) {
834 let space_delta =
835 Self::lookup_space_trigram_em(profile.space_trigrams, a, ch);
836 if space_delta != 0.0 {
837 em += space_delta;
838 } else if a == 'A' && ch == '(' {
839 em += profile.missing_space_after_capital_a_before_open_paren_em;
840 } else if ch == 'A' && a.is_ascii_alphanumeric() {
841 em += profile.missing_space_before_capital_a_em;
846 }
847 }
848 } else if !(a.is_whitespace() || b.is_whitespace() || ch.is_whitespace()) {
849 em += Self::same_glyph_trigram_em(profile, a, b, ch);
850 }
851 }
852 prevprev = prev;
853 prev = Some(ch);
854 same_run_len = next_same_run_len;
855 }
856 em * font_size
857 }
858
859 fn split_token_to_width_px(
860 profile: FontMetricProfile<'_>,
861 tok: &str,
862 max_width_px: f64,
863 bold: bool,
864 font_size: f64,
865 ) -> (String, String) {
866 fn normalize_whitespace_like(ch: char) -> (char, f64) {
867 const NBSP_DELTA_EM: f64 = -1.0 / 3072.0;
868 if ch == '\u{00A0}' {
869 (' ', NBSP_DELTA_EM)
870 } else {
871 (ch, 0.0)
872 }
873 }
874
875 if max_width_px <= 0.0 {
876 return (tok.to_string(), String::new());
877 }
878 let max_em = max_width_px / font_size.max(1.0);
879 let mut em = 0.0;
880 let mut prevprev: Option<char> = None;
881 let mut prev: Option<char> = None;
882 let mut same_run_len = 0usize;
883 let chars = tok.chars().collect::<Vec<_>>();
884 let mut split_at = 0usize;
885 for (idx, ch) in chars.iter().enumerate() {
886 let (ch_norm, delta_em) = normalize_whitespace_like(*ch);
887 let next_same_run_len = if prev == Some(ch_norm) {
888 same_run_len + 1
889 } else {
890 1
891 };
892 em += Self::lookup_char_em(profile.entries, profile.default_em, ch_norm) + delta_em;
893 if let Some(p) = prev {
894 em += Self::same_glyph_pair_kern_em(profile, p, ch_norm, next_same_run_len);
895 }
896 if bold {
897 if let Some(p) = prev {
898 em += flowchart_default_bold_kern_delta_em(p, ch_norm);
899 }
900 em += flowchart_default_bold_delta_em(ch_norm);
901 }
902 if let (Some(a), Some(b)) = (prevprev, prev) {
903 if !(a.is_whitespace() || b.is_whitespace() || ch_norm.is_whitespace()) {
904 em += Self::same_glyph_trigram_em(profile, a, b, ch_norm);
905 }
906 }
907 prevprev = prev;
908 prev = Some(ch_norm);
909 same_run_len = next_same_run_len;
910 if em > max_em && idx > 0 {
911 break;
912 }
913 split_at = idx + 1;
914 if em >= max_em {
915 break;
916 }
917 }
918 if split_at == 0 {
919 split_at = 1.min(chars.len());
920 }
921 let head = chars.iter().take(split_at).collect::<String>();
922 let tail = chars.iter().skip(split_at).collect::<String>();
923 (head, tail)
924 }
925
926 fn wrap_line_to_width_px(
927 profile: FontMetricProfile<'_>,
928 line: &str,
929 max_width_px: f64,
930 font_size: f64,
931 break_long_words: bool,
932 bold: bool,
933 ) -> Vec<String> {
934 fn split_html_breakable_segments(tok: &str) -> Vec<String> {
935 let hyphen_count = tok.chars().filter(|ch| *ch == '-').count();
943 let char_count = tok.chars().count();
944 let is_hyphenated_compound = hyphen_count >= 2 && char_count >= 16;
945 let is_url_like = tok.starts_with("http://") || tok.starts_with("https://");
946 let is_path_like = is_hyphenated_compound
947 || is_url_like
948 || tok.len() >= 24
949 && tok
950 .chars()
951 .filter(|ch| {
952 matches!(ch, '/' | '\\' | '-' | ':' | '?' | '&' | '#' | '[' | ']')
953 })
954 .count()
955 >= 2;
956 if !is_path_like {
957 return vec![tok.to_string()];
958 }
959
960 fn is_break_after(ch: char, is_url_like: bool) -> bool {
961 matches!(ch, '/' | '-' | ':' | '?' | '&' | '#' | ')' | ']' | '}')
962 || (is_url_like && ch == '.')
963 }
964
965 let mut out: Vec<String> = Vec::new();
966 let mut cur = String::new();
967 for ch in tok.chars() {
968 cur.push(ch);
969 if is_break_after(ch, is_url_like) && !cur.is_empty() {
970 out.push(std::mem::take(&mut cur));
971 }
972 }
973 if !cur.is_empty() {
974 out.push(cur);
975 }
976 if out.len() <= 1 {
977 vec![tok.to_string()]
978 } else {
979 out
980 }
981 }
982
983 let max_width_px = if break_long_words {
988 max_width_px
989 } else {
990 max_width_px + (1.0 / 64.0)
991 };
992
993 let mut tokens =
994 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
995 let mut out: Vec<String> = Vec::new();
996 let mut cur = String::new();
997
998 while let Some(tok) = tokens.pop_front() {
999 if cur.is_empty() && tok == " " {
1000 continue;
1001 }
1002
1003 let candidate = format!("{cur}{tok}");
1004 let candidate_trimmed = candidate.trim_end();
1005 if Self::line_width_px(profile, candidate_trimmed, bold, font_size) <= max_width_px {
1006 cur = candidate;
1007 continue;
1008 }
1009
1010 if !break_long_words && tok != " " && !cur.trim().is_empty() {
1011 let segments = split_html_breakable_segments(&tok);
1015 if segments.len() > 1 {
1016 let mut cur_candidate = cur.clone();
1017 let mut consumed = 0usize;
1018 for seg in &segments {
1019 let candidate = format!("{cur_candidate}{seg}");
1020 let candidate_trimmed = candidate.trim_end();
1021 if Self::line_width_px(profile, candidate_trimmed, bold, font_size)
1022 <= max_width_px
1023 {
1024 cur_candidate = candidate;
1025 consumed += 1;
1026 } else {
1027 break;
1028 }
1029 }
1030 if consumed > 0 {
1031 cur = cur_candidate;
1032 for seg in segments.into_iter().skip(consumed).rev() {
1033 tokens.push_front(seg);
1034 }
1035 continue;
1036 }
1037 }
1038 }
1039
1040 if !cur.trim().is_empty() {
1041 out.push(cur.trim_end().to_string());
1042 cur.clear();
1043 }
1044
1045 if tok == " " {
1046 continue;
1047 }
1048
1049 if Self::line_width_px(profile, tok.as_str(), bold, font_size) <= max_width_px {
1050 cur = tok;
1051 continue;
1052 }
1053
1054 if !break_long_words {
1055 let segments = split_html_breakable_segments(&tok);
1056 if segments.len() > 1 {
1057 for seg in segments.into_iter().rev() {
1058 tokens.push_front(seg);
1059 }
1060 continue;
1061 }
1062 out.push(tok);
1063 continue;
1064 }
1065
1066 let (head, tail) =
1067 Self::split_token_to_width_px(profile, &tok, max_width_px, bold, font_size);
1068 out.push(head);
1069 if !tail.is_empty() {
1070 tokens.push_front(tail);
1071 }
1072 }
1073
1074 if !cur.trim().is_empty() {
1075 out.push(cur.trim_end().to_string());
1076 }
1077
1078 if out.is_empty() {
1079 vec!["".to_string()]
1080 } else {
1081 out
1082 }
1083 }
1084
1085 fn wrap_text_lines_px(
1086 profile: FontMetricProfile<'_>,
1087 text: &str,
1088 style: &TextStyle,
1089 bold: bool,
1090 max_width_px: Option<f64>,
1091 wrap_mode: WrapMode,
1092 ) -> Vec<String> {
1093 let font_size = style.font_size.max(1.0);
1094 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
1095 let break_long_words = wrap_mode == WrapMode::SvgLike;
1096
1097 let mut lines = Vec::new();
1098 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1099 if let Some(w) = max_width_px {
1100 lines.extend(Self::wrap_line_to_width_px(
1101 profile,
1102 &line,
1103 w,
1104 font_size,
1105 break_long_words,
1106 bold,
1107 ));
1108 } else {
1109 lines.push(line);
1110 }
1111 }
1112
1113 if lines.is_empty() {
1114 vec!["".to_string()]
1115 } else {
1116 lines
1117 }
1118 }
1119}
1120
1121fn vendored_measure_wrapped_impl(
1122 measurer: &VendoredFontMetricsTextMeasurer,
1123 text: &str,
1124 style: &TextStyle,
1125 max_width: Option<f64>,
1126 wrap_mode: WrapMode,
1127 use_html_overrides: bool,
1128) -> (TextMetrics, Option<f64>) {
1129 let Some(table) = measurer.lookup_table(style) else {
1130 return measurer
1131 .fallback
1132 .measure_wrapped_with_raw_width(text, style, max_width, wrap_mode);
1133 };
1134
1135 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
1136 let font_size = style.font_size.max(1.0);
1137 let max_width = max_width.filter(|w| w.is_finite() && *w > 0.0);
1138 let line_height_factor = match wrap_mode {
1139 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => 1.1,
1140 WrapMode::HtmlLike => 1.5,
1141 };
1142
1143 let html_overrides: &[(&'static str, f64)] = if use_html_overrides && !bold {
1144 table.html_overrides
1145 } else {
1146 &[]
1147 };
1148 let profile = VendoredFontMetricsTextMeasurer::metric_profile(table);
1149
1150 let html_override_px = |em: f64| -> f64 {
1151 if (font_size - table.base_font_size_px).abs() < 0.01 {
1159 em * font_size
1160 } else {
1161 em * table.base_font_size_px
1162 }
1163 };
1164
1165 let html_width_override_px = |line: &str| -> Option<f64> {
1166 overrides::lookup_flowchart_html_width_px(table.font_key, font_size, line)
1169 };
1170
1171 let raw_width_unscaled = if wrap_mode == WrapMode::HtmlLike {
1180 let mut raw_w: f64 = 0.0;
1181 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1182 if let Some(w) = html_width_override_px(&line) {
1183 raw_w = raw_w.max(w);
1184 continue;
1185 }
1186 if let Some(em) =
1187 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, &line)
1188 {
1189 raw_w = raw_w.max(html_override_px(em));
1190 } else {
1191 raw_w = raw_w.max(VendoredFontMetricsTextMeasurer::line_width_px(
1192 profile, &line, bold, font_size,
1193 ));
1194 }
1195 }
1196 Some(raw_w)
1197 } else {
1198 None
1199 };
1200
1201 fn split_html_min_content_segments(tok: &str) -> Vec<String> {
1214 fn is_break_after(ch: char) -> bool {
1220 matches!(ch, '-' | '?' | '&' | '#')
1221 }
1222
1223 let mut out: Vec<String> = Vec::new();
1224 let mut cur = String::new();
1225 for ch in tok.chars() {
1226 cur.push(ch);
1227 if is_break_after(ch) && !cur.is_empty() {
1228 out.push(std::mem::take(&mut cur));
1229 }
1230 }
1231 if !cur.is_empty() {
1232 out.push(cur);
1233 }
1234 if out.len() <= 1 {
1235 vec![tok.to_string()]
1236 } else {
1237 out
1238 }
1239 }
1240
1241 let html_min_content_width = if wrap_mode == WrapMode::HtmlLike && max_width.is_some() {
1242 let mut max_word_w: f64 = 0.0;
1243 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1244 for part in line.split(' ') {
1245 let part = part.trim();
1246 if part.is_empty() {
1247 continue;
1248 }
1249 for seg in split_html_min_content_segments(part) {
1250 let seg_w = html_width_override_px(&seg).unwrap_or_else(|| {
1251 VendoredFontMetricsTextMeasurer::line_width_px(
1252 profile,
1253 seg.as_str(),
1254 bold,
1255 font_size,
1256 )
1257 });
1258 max_word_w = max_word_w.max(seg_w);
1259 }
1260 }
1261 }
1262 if max_word_w.is_finite() && max_word_w > 0.0 {
1263 Some(max_word_w)
1264 } else {
1265 None
1266 }
1267 } else {
1268 None
1269 };
1270
1271 let lines = match wrap_mode {
1272 WrapMode::HtmlLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_px(
1273 profile, text, style, bold, max_width, wrap_mode,
1274 ),
1275 WrapMode::SvgLike => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
1276 table, text, max_width, font_size, true,
1277 ),
1278 WrapMode::SvgLikeSingleRun => VendoredFontMetricsTextMeasurer::wrap_text_lines_svg_bbox_px(
1279 table, text, max_width, font_size, false,
1280 ),
1281 };
1282
1283 let mut width: f64 = 0.0;
1284 match wrap_mode {
1285 WrapMode::HtmlLike => {
1286 for line in &lines {
1287 if let Some(w) = html_width_override_px(line) {
1288 width = width.max(w);
1289 continue;
1290 }
1291 if let Some(em) =
1292 VendoredFontMetricsTextMeasurer::lookup_html_override_em(html_overrides, line)
1293 {
1294 width = width.max(html_override_px(em));
1295 } else {
1296 width = width.max(VendoredFontMetricsTextMeasurer::line_width_px(
1297 profile, line, bold, font_size,
1298 ));
1299 }
1300 }
1301 }
1302 WrapMode::SvgLike => {
1303 for line in &lines {
1304 width = width.max(VendoredFontMetricsTextMeasurer::line_svg_bbox_width_px(
1305 table, line, font_size,
1306 ));
1307 }
1308 }
1309 WrapMode::SvgLikeSingleRun => {
1310 for line in &lines {
1311 width = width.max(
1312 VendoredFontMetricsTextMeasurer::line_svg_bbox_width_single_run_px(
1313 table, line, font_size,
1314 ),
1315 );
1316 }
1317 }
1318 }
1319
1320 if wrap_mode == WrapMode::HtmlLike {
1324 let needs_wrap = max_width.is_some_and(|w| raw_width_unscaled.is_some_and(|rw| rw > w));
1325 if let Some(w) = max_width {
1326 if needs_wrap {
1327 width = width.max(w);
1328 } else {
1329 width = width.min(w);
1330 }
1331 }
1332 if needs_wrap {
1333 if let Some(w) = html_min_content_width {
1334 width = width.max(w);
1335 }
1336 }
1337 width = round_to_1_64_px(width);
1340 if let Some(w) = max_width {
1341 width = if needs_wrap {
1342 width.max(w)
1343 } else {
1344 width.min(w)
1345 };
1346 }
1347 }
1348
1349 let height = match wrap_mode {
1350 WrapMode::HtmlLike => lines.len() as f64 * font_size * line_height_factor,
1351 WrapMode::SvgLike | WrapMode::SvgLikeSingleRun => {
1352 if lines.is_empty() {
1353 0.0
1354 } else {
1355 let first_line_h = svg_wrapped_first_line_bbox_height_px(style);
1361 let additional = (lines.len().saturating_sub(1)) as f64 * font_size * 1.1;
1362 first_line_h + additional
1363 }
1364 }
1365 };
1366
1367 let metrics = TextMetrics {
1368 width,
1369 height,
1370 line_count: lines.len(),
1371 };
1372 let raw_width_px = if wrap_mode == WrapMode::HtmlLike {
1373 raw_width_unscaled
1374 } else {
1375 None
1376 };
1377 (metrics, raw_width_px)
1378}
1379
1380impl TextMeasurer for VendoredFontMetricsTextMeasurer {
1381 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
1382 self.measure_wrapped(text, style, None, WrapMode::SvgLike)
1383 }
1384
1385 fn measure_svg_text_computed_length_px(&self, text: &str, style: &TextStyle) -> f64 {
1386 let Some(table) = self.lookup_table(style) else {
1387 return self
1388 .fallback
1389 .measure_svg_text_computed_length_px(text, style);
1390 };
1391
1392 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
1393 let font_size = style.font_size.max(1.0);
1394 let profile = VendoredFontMetricsTextMeasurer::metric_profile(table);
1395 let mut width: f64 = 0.0;
1396 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1397 width = width.max(VendoredFontMetricsTextMeasurer::line_width_px(
1398 profile, &line, bold, font_size,
1399 ));
1400 }
1401 if width.is_finite() && width >= 0.0 {
1402 width
1403 } else {
1404 0.0
1405 }
1406 }
1407
1408 fn measure_svg_text_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
1409 let Some(table) = self.lookup_table(style) else {
1410 return self.fallback.measure_svg_text_bbox_x(text, style);
1411 };
1412
1413 let font_size = style.font_size.max(1.0);
1414 let mut left: f64 = 0.0;
1415 let mut right: f64 = 0.0;
1416 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1417 let (l, r) = Self::line_svg_bbox_extents_px(table, &line, font_size);
1418 left = left.max(l);
1419 right = right.max(r);
1420 }
1421 (left, right)
1422 }
1423
1424 fn measure_svg_text_bbox_x_with_ascii_overhang(
1425 &self,
1426 text: &str,
1427 style: &TextStyle,
1428 ) -> (f64, f64) {
1429 let Some(table) = self.lookup_table(style) else {
1430 return self
1431 .fallback
1432 .measure_svg_text_bbox_x_with_ascii_overhang(text, style);
1433 };
1434
1435 let font_size = style.font_size.max(1.0);
1436 let mut left: f64 = 0.0;
1437 let mut right: f64 = 0.0;
1438 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1439 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
1440 table, &line, font_size,
1441 );
1442 left = left.max(l);
1443 right = right.max(r);
1444 }
1445 (left, right)
1446 }
1447
1448 fn measure_svg_title_bbox_x(&self, text: &str, style: &TextStyle) -> (f64, f64) {
1449 let Some(table) = self.lookup_table(style) else {
1450 return self.fallback.measure_svg_title_bbox_x(text, style);
1451 };
1452
1453 let font_size = style.font_size.max(1.0);
1454 let mut left: f64 = 0.0;
1455 let mut right: f64 = 0.0;
1456 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1457 let (l, r) = Self::line_svg_title_bbox_extents_px(table, &line, font_size);
1458 left = left.max(l);
1459 right = right.max(r);
1460 }
1461 (left, right)
1462 }
1463
1464 fn measure_svg_simple_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
1465 let Some(table) = self.lookup_table(style) else {
1466 return self
1467 .fallback
1468 .measure_svg_simple_text_bbox_width_px(text, style);
1469 };
1470
1471 let font_size = style.font_size.max(1.0);
1472 let t = text.trim_end();
1473 if !t.is_empty() {
1474 if let Some((left_em, right_em)) =
1475 overrides::lookup_sequence_svg_override_em(table.font_key, t)
1476 {
1477 let left = Self::quantize_svg_bbox_px_nearest((left_em * font_size).max(0.0));
1478 let right = Self::quantize_svg_bbox_px_nearest((right_em * font_size).max(0.0));
1479 return (left + right).max(0.0);
1480 }
1481 }
1482
1483 let mut width: f64 = 0.0;
1484 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1485 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
1486 table, &line, font_size,
1487 );
1488 width = width.max((l + r).max(0.0));
1489 }
1490 width
1491 }
1492
1493 fn measure_svg_simple_text_bbox_width_for_wrap_px(&self, text: &str, style: &TextStyle) -> f64 {
1494 let Some(table) = self.lookup_table(style) else {
1495 return self
1496 .fallback
1497 .measure_svg_simple_text_bbox_width_for_wrap_px(text, style);
1498 };
1499
1500 let font_size = style.font_size.max(1.0);
1501 let mut width: f64 = 0.0;
1502 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1503 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang(
1504 table, &line, font_size,
1505 );
1506 width = width.max((l + r).max(0.0));
1507 }
1508 width
1509 }
1510
1511 fn measure_svg_raw_text_bbox_width_px(&self, text: &str, style: &TextStyle) -> f64 {
1512 let Some(table) = self.lookup_table(style) else {
1513 return self
1514 .fallback
1515 .measure_svg_raw_text_bbox_width_px(text, style);
1516 };
1517
1518 let font_size = style.font_size.max(1.0);
1519 let bold = is_flowchart_default_font(style) && style_requests_bold_font_weight(style);
1520 let mut width: f64 = 0.0;
1521 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
1522 let (l, r) = Self::line_svg_bbox_extents_px_single_run_with_ascii_overhang_and_weight(
1523 table, &line, font_size, bold,
1524 );
1525 width = width.max((l + r).max(0.0));
1526 }
1527 width
1528 }
1529
1530 fn measure_svg_simple_text_bbox_height_px(&self, text: &str, style: &TextStyle) -> f64 {
1531 let t = text.trim_end();
1532 if t.is_empty() {
1533 return 0.0;
1534 }
1535 let font_size = style.font_size.max(1.0);
1538 (font_size * 1.1).max(0.0)
1539 }
1540
1541 fn measure_wrapped(
1542 &self,
1543 text: &str,
1544 style: &TextStyle,
1545 max_width: Option<f64>,
1546 wrap_mode: WrapMode,
1547 ) -> TextMetrics {
1548 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true).0
1549 }
1550
1551 fn measure_wrapped_with_raw_width(
1552 &self,
1553 text: &str,
1554 style: &TextStyle,
1555 max_width: Option<f64>,
1556 wrap_mode: WrapMode,
1557 ) -> (TextMetrics, Option<f64>) {
1558 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, true)
1559 }
1560
1561 fn measure_wrapped_raw(
1562 &self,
1563 text: &str,
1564 style: &TextStyle,
1565 max_width: Option<f64>,
1566 wrap_mode: WrapMode,
1567 ) -> TextMetrics {
1568 vendored_measure_wrapped_impl(self, text, style, max_width, wrap_mode, false).0
1569 }
1570}