1use crate::text_layout_result::TextLayoutResult;
2use cranpose_core::NodeId;
3use std::borrow::Cow;
4use std::cell::RefCell;
5use std::ops::Range;
6
7use super::layout_options::{TextLayoutOptions, TextOverflow};
8use super::paragraph::{Hyphens, LineBreak};
9use super::style::TextStyle;
10
11const ELLIPSIS: &str = "\u{2026}";
12const DEFAULT_FONT_SIZE_SP: f32 = 14.0;
13const WRAP_EPSILON: f32 = 0.5;
14const SCALE_DOWN_SEARCH_STEPS: usize = 14;
15const AUTO_HYPHEN_MIN_SEGMENT_CHARS: usize = 2;
16const AUTO_HYPHEN_MIN_TRAILING_CHARS: usize = 3;
17const AUTO_HYPHEN_PREFERRED_TRAILING_CHARS: usize = 4;
18
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct TextMetrics {
21 pub width: f32,
22 pub height: f32,
23 pub line_height: f32,
25 pub line_count: usize,
27}
28
29#[derive(Clone, Debug, PartialEq)]
30pub struct PreparedTextLayout {
31 pub text: crate::text::AnnotatedString,
32 pub visual_style: TextStyle,
33 pub metrics: TextMetrics,
34 pub did_overflow: bool,
35}
36
37pub trait TextMeasurer: 'static {
38 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
39
40 fn measure_for_node(
41 &self,
42 node_id: Option<NodeId>,
43 text: &crate::text::AnnotatedString,
44 style: &TextStyle,
45 ) -> TextMetrics {
46 let _ = node_id;
47 self.measure(text, style)
48 }
49
50 fn measure_subsequence(
51 &self,
52 text: &crate::text::AnnotatedString,
53 range: Range<usize>,
54 style: &TextStyle,
55 ) -> TextMetrics {
56 self.measure(&text.subsequence(range), style)
57 }
58
59 fn measure_subsequence_for_node(
60 &self,
61 node_id: Option<NodeId>,
62 text: &crate::text::AnnotatedString,
63 range: Range<usize>,
64 style: &TextStyle,
65 ) -> TextMetrics {
66 let _ = node_id;
67 self.measure_subsequence(text, range, style)
68 }
69
70 fn get_offset_for_position(
71 &self,
72 text: &crate::text::AnnotatedString,
73 style: &TextStyle,
74 x: f32,
75 y: f32,
76 ) -> usize;
77
78 fn get_cursor_x_for_offset(
79 &self,
80 text: &crate::text::AnnotatedString,
81 style: &TextStyle,
82 offset: usize,
83 ) -> f32;
84
85 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
86
87 fn choose_auto_hyphen_break(
93 &self,
94 _line: &str,
95 _style: &TextStyle,
96 _segment_start_char: usize,
97 _measured_break_char: usize,
98 ) -> Option<usize> {
99 None
100 }
101
102 fn measure_with_options(
103 &self,
104 text: &crate::text::AnnotatedString,
105 style: &TextStyle,
106 options: TextLayoutOptions,
107 max_width: Option<f32>,
108 ) -> TextMetrics {
109 self.prepare_with_options(text, style, options, max_width)
110 .metrics
111 }
112
113 fn measure_with_options_for_node(
114 &self,
115 node_id: Option<NodeId>,
116 text: &crate::text::AnnotatedString,
117 style: &TextStyle,
118 options: TextLayoutOptions,
119 max_width: Option<f32>,
120 ) -> TextMetrics {
121 self.prepare_with_options_for_node(node_id, text, style, options, max_width)
122 .metrics
123 }
124
125 fn prepare_with_options(
126 &self,
127 text: &crate::text::AnnotatedString,
128 style: &TextStyle,
129 options: TextLayoutOptions,
130 max_width: Option<f32>,
131 ) -> PreparedTextLayout {
132 self.prepare_with_options_fallback(text, style, options, max_width)
133 }
134
135 fn prepare_with_options_for_node(
136 &self,
137 node_id: Option<NodeId>,
138 text: &crate::text::AnnotatedString,
139 style: &TextStyle,
140 options: TextLayoutOptions,
141 max_width: Option<f32>,
142 ) -> PreparedTextLayout {
143 prepare_text_layout_with_measurer_for_node(self, node_id, text, style, options, max_width)
144 }
145
146 fn prepare_with_options_fallback(
147 &self,
148 text: &crate::text::AnnotatedString,
149 style: &TextStyle,
150 options: TextLayoutOptions,
151 max_width: Option<f32>,
152 ) -> PreparedTextLayout {
153 prepare_text_layout_fallback(self, text, style, options, max_width)
154 }
155}
156
157#[derive(Default)]
158struct MonospacedTextMeasurer;
159
160impl MonospacedTextMeasurer {
161 const DEFAULT_SIZE: f32 = 14.0;
162 const CHAR_WIDTH_RATIO: f32 = 0.6; fn get_metrics(style: &TextStyle) -> (f32, f32) {
165 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
166 let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
167 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
168 (
169 (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
170 line_height,
171 )
172 }
173}
174
175impl TextMeasurer for MonospacedTextMeasurer {
176 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
177 let (char_width, line_height) = Self::get_metrics(style);
178
179 let lines: Vec<&str> = text.text.split('\n').collect();
180 let line_count = lines.len().max(1);
181
182 let width = lines
183 .iter()
184 .map(|line| line.chars().count() as f32 * char_width)
185 .fold(0.0_f32, f32::max);
186
187 TextMetrics {
188 width,
189 height: line_count as f32 * line_height,
190 line_height,
191 line_count,
192 }
193 }
194
195 fn measure_subsequence(
196 &self,
197 text: &crate::text::AnnotatedString,
198 range: Range<usize>,
199 style: &TextStyle,
200 ) -> TextMetrics {
201 let (char_width, line_height) = Self::get_metrics(style);
202 let slice = &text.text[range];
203 let line_count = slice.split('\n').count().max(1);
204 let width = slice
205 .split('\n')
206 .map(|line| line.chars().count() as f32 * char_width)
207 .fold(0.0_f32, f32::max);
208
209 TextMetrics {
210 width,
211 height: line_count as f32 * line_height,
212 line_height,
213 line_count,
214 }
215 }
216
217 fn get_offset_for_position(
218 &self,
219 text: &crate::text::AnnotatedString,
220 style: &TextStyle,
221 x: f32,
222 y: f32,
223 ) -> usize {
224 let (char_width, line_height) = Self::get_metrics(style);
225
226 if text.text.is_empty() {
227 return 0;
228 }
229
230 let line_index = (y / line_height).floor().max(0.0) as usize;
231 let lines: Vec<&str> = text.text.split('\n').collect();
232 let target_line = line_index.min(lines.len().saturating_sub(1));
233
234 let mut line_start_byte = 0;
235 for line in lines.iter().take(target_line) {
236 line_start_byte += line.len() + 1;
237 }
238
239 let line_text = lines.get(target_line).unwrap_or(&"");
240 let char_index = (x / char_width).round() as usize;
241 let line_char_count = line_text.chars().count();
242 let clamped_index = char_index.min(line_char_count);
243
244 let offset_in_line = line_text
245 .char_indices()
246 .nth(clamped_index)
247 .map(|(i, _)| i)
248 .unwrap_or(line_text.len());
249
250 line_start_byte + offset_in_line
251 }
252
253 fn get_cursor_x_for_offset(
254 &self,
255 text: &crate::text::AnnotatedString,
256 style: &TextStyle,
257 offset: usize,
258 ) -> f32 {
259 let (char_width, _) = Self::get_metrics(style);
260
261 let clamped_offset = offset.min(text.text.len());
262 let char_count = text.text[..clamped_offset].chars().count();
263 char_count as f32 * char_width
264 }
265
266 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
267 let (char_width, line_height) = Self::get_metrics(style);
268 TextLayoutResult::monospaced(&text.text, char_width, line_height)
269 }
270}
271
272thread_local! {
273 static TEXT_MEASURER: RefCell<Box<dyn TextMeasurer>> = RefCell::new(Box::new(MonospacedTextMeasurer));
274}
275
276pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
277 TEXT_MEASURER.with(|m| {
278 *m.borrow_mut() = Box::new(measurer);
279 });
280}
281
282pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
283 TEXT_MEASURER.with(|m| m.borrow().measure(text, style))
284}
285
286pub fn measure_text_for_node(
287 node_id: Option<NodeId>,
288 text: &crate::text::AnnotatedString,
289 style: &TextStyle,
290) -> TextMetrics {
291 TEXT_MEASURER.with(|m| m.borrow().measure_for_node(node_id, text, style))
292}
293
294pub fn measure_text_with_options(
295 text: &crate::text::AnnotatedString,
296 style: &TextStyle,
297 options: TextLayoutOptions,
298 max_width: Option<f32>,
299) -> TextMetrics {
300 TEXT_MEASURER.with(|m| {
301 m.borrow()
302 .measure_with_options(text, style, options.normalized(), max_width)
303 })
304}
305
306pub fn measure_text_with_options_for_node(
307 node_id: Option<NodeId>,
308 text: &crate::text::AnnotatedString,
309 style: &TextStyle,
310 options: TextLayoutOptions,
311 max_width: Option<f32>,
312) -> TextMetrics {
313 TEXT_MEASURER.with(|m| {
314 m.borrow().measure_with_options_for_node(
315 node_id,
316 text,
317 style,
318 options.normalized(),
319 max_width,
320 )
321 })
322}
323
324pub fn prepare_text_layout(
325 text: &crate::text::AnnotatedString,
326 style: &TextStyle,
327 options: TextLayoutOptions,
328 max_width: Option<f32>,
329) -> PreparedTextLayout {
330 TEXT_MEASURER.with(|m| {
331 m.borrow()
332 .prepare_with_options(text, style, options.normalized(), max_width)
333 })
334}
335
336pub fn prepare_text_layout_for_node(
337 node_id: Option<NodeId>,
338 text: &crate::text::AnnotatedString,
339 style: &TextStyle,
340 options: TextLayoutOptions,
341 max_width: Option<f32>,
342) -> PreparedTextLayout {
343 TEXT_MEASURER.with(|m| {
344 m.borrow().prepare_with_options_for_node(
345 node_id,
346 text,
347 style,
348 options.normalized(),
349 max_width,
350 )
351 })
352}
353
354pub fn get_offset_for_position(
355 text: &crate::text::AnnotatedString,
356 style: &TextStyle,
357 x: f32,
358 y: f32,
359) -> usize {
360 TEXT_MEASURER.with(|m| m.borrow().get_offset_for_position(text, style, x, y))
361}
362
363pub fn get_cursor_x_for_offset(
364 text: &crate::text::AnnotatedString,
365 style: &TextStyle,
366 offset: usize,
367) -> f32 {
368 TEXT_MEASURER.with(|m| m.borrow().get_cursor_x_for_offset(text, style, offset))
369}
370
371pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
372 TEXT_MEASURER.with(|m| m.borrow().layout(text, style))
373}
374
375fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
376 measurer: &M,
377 text: &crate::text::AnnotatedString,
378 style: &TextStyle,
379 options: TextLayoutOptions,
380 max_width: Option<f32>,
381) -> PreparedTextLayout {
382 prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
383}
384
385pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
386 measurer: &M,
387 node_id: Option<NodeId>,
388 text: &crate::text::AnnotatedString,
389 style: &TextStyle,
390 options: TextLayoutOptions,
391 max_width: Option<f32>,
392) -> PreparedTextLayout {
393 let opts = options.normalized();
394 let max_width = normalize_max_width(max_width);
395 if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
396 return prepare_scale_down_text_layout(
397 measurer,
398 node_id,
399 text,
400 style,
401 opts,
402 max_width,
403 min_font_size_sp,
404 );
405 }
406
407 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
408 .then_some(max_width)
409 .flatten();
410 let line_break_mode = style
411 .paragraph_style
412 .line_break
413 .take_or_else(|| LineBreak::Simple);
414 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
415
416 let mut line_ranges = split_line_ranges(text.text.as_str());
417 if let Some(width_limit) = wrap_width {
418 let mut wrapped_ranges = Vec::with_capacity(line_ranges.len());
419 for line_range in line_ranges.drain(..) {
420 let wrapped_line_ranges = wrap_line_to_width(
421 measurer,
422 text,
423 line_range,
424 style,
425 width_limit,
426 line_break_mode,
427 hyphens_mode,
428 );
429 wrapped_ranges.extend(wrapped_line_ranges);
430 }
431 line_ranges = wrapped_ranges;
432 }
433
434 let mut did_overflow = false;
435 let mut visible_lines: Vec<DisplayLine> = line_ranges
436 .into_iter()
437 .map(DisplayLine::from_source_range)
438 .collect();
439
440 if opts.overflow != TextOverflow::Visible && visible_lines.len() > opts.max_lines {
441 did_overflow = true;
442 visible_lines.truncate(opts.max_lines);
443 if let Some(last_line) = visible_lines.last_mut() {
444 let overflowed = apply_line_overflow(
445 measurer,
446 last_line.display_text(text),
447 style,
448 max_width,
449 opts,
450 true,
451 true,
452 );
453 last_line.apply_display_text(text, overflowed);
454 }
455 }
456
457 if let Some(width_limit) = max_width {
458 let single_line_ellipsis = opts.max_lines == 1 || !opts.soft_wrap;
459 let visible_len = visible_lines.len();
460 for (line_index, line) in visible_lines.iter_mut().enumerate() {
461 let width = line.measure_width(measurer, node_id, text, style);
462 if width > width_limit + WRAP_EPSILON {
463 if opts.overflow == TextOverflow::Visible {
464 continue;
465 }
466 did_overflow = true;
467 let overflowed = apply_line_overflow(
468 measurer,
469 line.display_text(text),
470 style,
471 Some(width_limit),
472 opts,
473 line_index + 1 == visible_len,
474 single_line_ellipsis,
475 );
476 line.apply_display_text(text, overflowed);
477 }
478 }
479 }
480
481 let display_annotated = build_display_annotated(text, &visible_lines);
482 debug_assert_eq!(
483 display_annotated.text,
484 join_display_line_text(text, &visible_lines)
485 );
486 let line_height = measurer
487 .measure_for_node(node_id, text, style)
488 .line_height
489 .max(0.0);
490 let display_line_count = visible_lines.len().max(1);
491 let layout_line_count = display_line_count.max(opts.min_lines);
492
493 let measured_width = if visible_lines.is_empty() {
494 0.0
495 } else {
496 visible_lines
497 .iter()
498 .map(|line| line.measure_width(measurer, node_id, text, style))
499 .fold(0.0_f32, f32::max)
500 };
501 let width = if opts.overflow == TextOverflow::Visible {
502 measured_width
503 } else if let Some(width_limit) = max_width {
504 measured_width.min(width_limit)
505 } else {
506 measured_width
507 };
508
509 PreparedTextLayout {
510 text: display_annotated,
511 visual_style: style.clone(),
512 metrics: TextMetrics {
513 width,
514 height: layout_line_count as f32 * line_height,
515 line_height,
516 line_count: layout_line_count,
517 },
518 did_overflow,
519 }
520}
521
522fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
523 measurer: &M,
524 node_id: Option<NodeId>,
525 text: &crate::text::AnnotatedString,
526 style: &TextStyle,
527 options: TextLayoutOptions,
528 max_width: Option<f32>,
529 min_font_size_sp: f32,
530) -> PreparedTextLayout {
531 let clipped_options = TextLayoutOptions {
532 overflow: TextOverflow::Clip,
533 ..options
534 }
535 .normalized();
536
537 let full_size = prepare_scaled_text_layout(
538 measurer,
539 node_id,
540 text,
541 style,
542 clipped_options,
543 max_width,
544 1.0,
545 );
546 let Some(width_limit) = max_width else {
547 return full_size;
548 };
549 if !full_size.did_overflow {
550 return full_size;
551 }
552
553 let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
554 if !base_font_size.is_finite() || base_font_size <= 0.0 {
555 return full_size;
556 }
557 let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
558 if min_scale >= 1.0 {
559 return full_size;
560 }
561
562 let min_size = prepare_scaled_text_layout(
563 measurer,
564 node_id,
565 text,
566 style,
567 clipped_options,
568 Some(width_limit),
569 min_scale,
570 );
571 if min_size.did_overflow {
572 return min_size;
573 }
574
575 let mut low = min_scale;
576 let mut high = 1.0;
577 let mut best = min_size;
578 for _ in 0..SCALE_DOWN_SEARCH_STEPS {
579 let mid = (low + high) * 0.5;
580 let candidate = prepare_scaled_text_layout(
581 measurer,
582 node_id,
583 text,
584 style,
585 clipped_options,
586 Some(width_limit),
587 mid,
588 );
589 if candidate.did_overflow {
590 high = mid;
591 } else {
592 low = mid;
593 best = candidate;
594 }
595 }
596
597 best
598}
599
600fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
601 measurer: &M,
602 node_id: Option<NodeId>,
603 text: &crate::text::AnnotatedString,
604 style: &TextStyle,
605 options: TextLayoutOptions,
606 max_width: Option<f32>,
607 font_scale: f32,
608) -> PreparedTextLayout {
609 let visual_style = scale_text_style_font_sizes(style, font_scale);
610 let visual_text = scale_annotated_font_sizes(text, font_scale);
611 prepare_text_layout_with_measurer_for_node(
612 measurer,
613 node_id,
614 visual_text.as_ref(),
615 &visual_style,
616 options,
617 max_width,
618 )
619}
620
621fn scale_annotated_font_sizes(
622 text: &crate::text::AnnotatedString,
623 factor: f32,
624) -> Cow<'_, crate::text::AnnotatedString> {
625 if is_identity_scale(factor) || !annotated_text_needs_scaling(text) {
626 return Cow::Borrowed(text);
627 }
628
629 let mut scaled = text.clone();
630 for span in &mut scaled.span_styles {
631 span.item = scale_span_style_font_sizes(&span.item, factor, None);
632 }
633 Cow::Owned(scaled)
634}
635
636fn scale_text_style_font_sizes(style: &TextStyle, factor: f32) -> TextStyle {
637 if is_identity_scale(factor) {
638 return style.clone();
639 }
640
641 let mut scaled = style.clone();
642 scaled.span_style =
643 scale_span_style_font_sizes(&style.span_style, factor, Some(DEFAULT_FONT_SIZE_SP));
644 scaled.paragraph_style.line_height =
645 scale_text_unit_sp(scaled.paragraph_style.line_height, factor);
646 if let Some(mut indent) = scaled.paragraph_style.text_indent {
647 indent.first_line = scale_text_unit_sp(indent.first_line, factor);
648 indent.rest_line = scale_text_unit_sp(indent.rest_line, factor);
649 scaled.paragraph_style.text_indent = Some(indent);
650 }
651 scaled
652}
653
654fn scale_span_style_font_sizes(
655 style: &crate::text::SpanStyle,
656 factor: f32,
657 default_font_size_sp: Option<f32>,
658) -> crate::text::SpanStyle {
659 let mut scaled = style.clone();
660 scaled.font_size = match (style.font_size, default_font_size_sp) {
661 (crate::text::TextUnit::Unspecified, Some(default_size)) => {
662 crate::text::TextUnit::Sp(default_size * factor)
663 }
664 (unit, Some(_)) => scale_text_unit_sp_and_em(unit, factor),
665 (unit, None) => scale_text_unit_sp(unit, factor),
666 };
667 scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, factor);
668 if let Some(mut shadow) = scaled.shadow {
669 shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
670 shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
671 shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
672 scaled.shadow = Some(shadow);
673 }
674 if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
675 scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
676 width: width * factor,
677 });
678 }
679 scaled
680}
681
682fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
683 text.span_styles
684 .iter()
685 .any(|span| span_style_needs_scaling(&span.item))
686}
687
688fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
689 matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
690 || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
691 || matches!(
692 style.draw_style,
693 Some(crate::text::TextDrawStyle::Stroke { .. })
694 )
695 || style.shadow.is_some()
696}
697
698fn scale_text_unit_sp(unit: crate::text::TextUnit, factor: f32) -> crate::text::TextUnit {
699 match unit {
700 crate::text::TextUnit::Sp(value) if value.is_finite() => {
701 crate::text::TextUnit::Sp(value * factor)
702 }
703 other => other,
704 }
705}
706
707fn scale_text_unit_sp_and_em(unit: crate::text::TextUnit, factor: f32) -> crate::text::TextUnit {
708 match unit {
709 crate::text::TextUnit::Sp(value) if value.is_finite() => {
710 crate::text::TextUnit::Sp(value * factor)
711 }
712 crate::text::TextUnit::Em(value) if value.is_finite() => {
713 crate::text::TextUnit::Em(value * factor)
714 }
715 other => other,
716 }
717}
718
719fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
720 if value.is_finite() {
721 value * factor
722 } else {
723 value
724 }
725}
726
727fn is_identity_scale(factor: f32) -> bool {
728 (factor - 1.0).abs() <= f32::EPSILON
729}
730
731#[derive(Clone, Debug)]
732enum DisplayLineText {
733 Source,
734 Remapped(crate::text::AnnotatedString),
735}
736
737#[derive(Clone, Debug)]
738struct DisplayLine {
739 source_range: Range<usize>,
740 text: DisplayLineText,
741}
742
743impl DisplayLine {
744 fn from_source_range(source_range: Range<usize>) -> Self {
745 Self {
746 source_range,
747 text: DisplayLineText::Source,
748 }
749 }
750
751 fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
752 match &self.text {
753 DisplayLineText::Source => &source.text[self.source_range.clone()],
754 DisplayLineText::Remapped(annotated) => annotated.text.as_str(),
755 }
756 }
757
758 fn measure_width<M: TextMeasurer + ?Sized>(
759 &self,
760 measurer: &M,
761 node_id: Option<NodeId>,
762 source: &crate::text::AnnotatedString,
763 style: &TextStyle,
764 ) -> f32 {
765 match &self.text {
766 DisplayLineText::Source => {
767 measurer
768 .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
769 .width
770 }
771 DisplayLineText::Remapped(annotated) => {
772 measurer.measure_for_node(node_id, annotated, style).width
773 }
774 }
775 }
776
777 fn apply_display_text(&mut self, source: &crate::text::AnnotatedString, display_text: String) {
778 let source_text = &source.text[self.source_range.clone()];
779 self.text = if source_text == display_text {
780 DisplayLineText::Source
781 } else {
782 DisplayLineText::Remapped(remap_annotated_subsequence_for_display(
783 source,
784 self.source_range.clone(),
785 display_text.as_str(),
786 ))
787 };
788 }
789}
790
791fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
792 if text.is_empty() {
793 return single_line_range(0..0);
794 }
795
796 let mut ranges = Vec::new();
797 let mut start = 0usize;
798 for (idx, ch) in text.char_indices() {
799 if ch == '\n' {
800 ranges.push(start..idx);
801 start = idx + ch.len_utf8();
802 }
803 }
804 ranges.push(start..text.len());
805 ranges
806}
807
808fn build_display_annotated(
809 source: &crate::text::AnnotatedString,
810 lines: &[DisplayLine],
811) -> crate::text::AnnotatedString {
812 if lines.is_empty() {
813 return crate::text::AnnotatedString::from("");
814 }
815
816 let mut builder = crate::text::AnnotatedString::builder();
817 for (idx, line) in lines.iter().enumerate() {
818 builder = match &line.text {
819 DisplayLineText::Source => {
820 builder.append_annotated_subsequence(source, line.source_range.clone())
821 }
822 DisplayLineText::Remapped(annotated) => builder.append_annotated(annotated),
823 };
824 if idx + 1 < lines.len() {
825 builder = builder.append("\n");
826 }
827 }
828 builder.to_annotated_string()
829}
830
831fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
832 let mut text = String::new();
833 for (idx, line) in lines.iter().enumerate() {
834 text.push_str(line.display_text(source));
835 if idx + 1 < lines.len() {
836 text.push('\n');
837 }
838 }
839 text
840}
841
842fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
843 while end > start {
844 let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
845 break;
846 };
847 if ch.is_whitespace() {
848 end = start + idx;
849 } else {
850 break;
851 }
852 }
853 end
854}
855
856fn remap_annotated_subsequence_for_display(
857 source: &crate::text::AnnotatedString,
858 source_range: Range<usize>,
859 display_text: &str,
860) -> crate::text::AnnotatedString {
861 let source_text = &source.text[source_range.clone()];
862 if source_text == display_text {
863 return source.subsequence(source_range);
864 }
865
866 let display_chars = map_display_chars_to_source(source_text, display_text);
867 crate::text::AnnotatedString {
868 text: display_text.to_string(),
869 span_styles: remap_subsequence_range_styles(
870 &source.span_styles,
871 source_range.clone(),
872 &display_chars,
873 ),
874 paragraph_styles: remap_subsequence_range_styles(
875 &source.paragraph_styles,
876 source_range.clone(),
877 &display_chars,
878 ),
879 string_annotations: remap_subsequence_range_styles(
880 &source.string_annotations,
881 source_range.clone(),
882 &display_chars,
883 ),
884 link_annotations: remap_subsequence_range_styles(
885 &source.link_annotations,
886 source_range,
887 &display_chars,
888 ),
889 }
890}
891
892#[derive(Clone, Copy)]
893struct DisplayCharMap {
894 display_start: usize,
895 display_end: usize,
896 source_start: Option<usize>,
897}
898
899fn map_display_chars_to_source(source: &str, display: &str) -> Vec<DisplayCharMap> {
900 let source_chars: Vec<(usize, char)> = source.char_indices().collect();
901 let mut source_index = 0usize;
902 let mut maps = Vec::with_capacity(display.chars().count());
903
904 for (display_start, display_char) in display.char_indices() {
905 let display_end = display_start + display_char.len_utf8();
906 let mut source_start = None;
907 while source_index < source_chars.len() {
908 let (candidate_start, candidate_char) = source_chars[source_index];
909 source_index += 1;
910 if candidate_char == display_char {
911 source_start = Some(candidate_start);
912 break;
913 }
914 }
915 maps.push(DisplayCharMap {
916 display_start,
917 display_end,
918 source_start,
919 });
920 }
921
922 maps
923}
924
925fn remap_subsequence_range_styles<T: Clone>(
926 styles: &[crate::text::RangeStyle<T>],
927 source_range: Range<usize>,
928 display_chars: &[DisplayCharMap],
929) -> Vec<crate::text::RangeStyle<T>> {
930 let mut remapped = Vec::new();
931
932 for style in styles {
933 let overlap_start = style.range.start.max(source_range.start);
934 let overlap_end = style.range.end.min(source_range.end);
935 if overlap_start >= overlap_end {
936 continue;
937 }
938 let local_source_range =
939 (overlap_start - source_range.start)..(overlap_end - source_range.start);
940 let mut range_start = None;
941 let mut range_end = 0usize;
942
943 for map in display_chars {
944 let in_range = map.source_start.is_some_and(|source_start| {
945 source_start >= local_source_range.start && source_start < local_source_range.end
946 });
947
948 if in_range {
949 if range_start.is_none() {
950 range_start = Some(map.display_start);
951 }
952 range_end = map.display_end;
953 continue;
954 }
955
956 if let Some(start) = range_start.take() {
957 if start < range_end {
958 remapped.push(crate::text::RangeStyle {
959 item: style.item.clone(),
960 range: start..range_end,
961 });
962 }
963 }
964 }
965
966 if let Some(start) = range_start.take() {
967 if start < range_end {
968 remapped.push(crate::text::RangeStyle {
969 item: style.item.clone(),
970 range: start..range_end,
971 });
972 }
973 }
974 }
975
976 remapped
977}
978
979fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
980 match max_width {
981 Some(width) if width.is_finite() && width > 0.0 => Some(width),
982 _ => None,
983 }
984}
985
986fn absolute_range(base: &Range<usize>, relative: Range<usize>) -> Range<usize> {
987 (base.start + relative.start)..(base.start + relative.end)
988}
989
990fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
991 std::iter::once(range).collect()
992}
993
994fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
995 measurer: &M,
996 text: &crate::text::AnnotatedString,
997 line_range: Range<usize>,
998 style: &TextStyle,
999 max_width: f32,
1000 line_break: LineBreak,
1001 hyphens: Hyphens,
1002) -> Vec<Range<usize>> {
1003 let line_text = &text.text[line_range.clone()];
1004 if line_text.is_empty() {
1005 return single_line_range(line_range.start..line_range.start);
1006 }
1007
1008 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1009 && line_text.chars().any(char::is_whitespace)
1010 {
1011 if let Some(balanced) = wrap_line_with_word_balance(
1012 measurer,
1013 text,
1014 line_range.clone(),
1015 style,
1016 max_width,
1017 line_break,
1018 ) {
1019 return balanced;
1020 }
1021 }
1022
1023 wrap_line_greedy(
1024 measurer, text, line_range, style, max_width, line_break, hyphens,
1025 )
1026}
1027
1028fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1029 measurer: &M,
1030 text: &crate::text::AnnotatedString,
1031 line_range: Range<usize>,
1032 style: &TextStyle,
1033 max_width: f32,
1034 line_break: LineBreak,
1035 hyphens: Hyphens,
1036) -> Vec<Range<usize>> {
1037 let line_text = &text.text[line_range.clone()];
1038 let boundaries = char_boundaries(line_text);
1039 let mut wrapped = Vec::new();
1040 let mut start_idx = 0usize;
1041
1042 while start_idx < boundaries.len() - 1 {
1043 let mut low = start_idx + 1;
1044 let mut high = boundaries.len() - 1;
1045 let mut best = start_idx + 1;
1046
1047 while low <= high {
1048 let mid = (low + high) / 2;
1049 let segment_range = absolute_range(&line_range, boundaries[start_idx]..boundaries[mid]);
1050 let width = measurer
1051 .measure_subsequence(text, segment_range, style)
1052 .width;
1053 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1054 best = mid;
1055 low = mid + 1;
1056 } else {
1057 if mid == 0 {
1058 break;
1059 }
1060 high = mid - 1;
1061 }
1062 }
1063
1064 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1065 let mut effective_wrap_idx = wrap_idx;
1066 let can_hyphenate = hyphens == Hyphens::Auto
1067 && wrap_idx == best
1068 && best < boundaries.len() - 1
1069 && is_break_inside_word(line_text, &boundaries, wrap_idx);
1070 if can_hyphenate {
1071 effective_wrap_idx = resolve_auto_hyphen_break(
1072 measurer,
1073 line_text,
1074 style,
1075 &boundaries,
1076 start_idx,
1077 wrap_idx,
1078 );
1079 }
1080
1081 let segment_start = boundaries[start_idx];
1082 let mut segment_end = boundaries[effective_wrap_idx];
1083 if wrap_idx != best {
1084 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1085 }
1086 wrapped.push(absolute_range(&line_range, segment_start..segment_end));
1087
1088 start_idx = if wrap_idx != best {
1089 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1090 } else {
1091 effective_wrap_idx
1092 };
1093 }
1094
1095 if wrapped.is_empty() {
1096 wrapped.push(line_range.start..line_range.start);
1097 }
1098
1099 wrapped
1100}
1101
1102fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1103 measurer: &M,
1104 text: &crate::text::AnnotatedString,
1105 line_range: Range<usize>,
1106 style: &TextStyle,
1107 max_width: f32,
1108 line_break: LineBreak,
1109) -> Option<Vec<Range<usize>>> {
1110 let line_text = &text.text[line_range.clone()];
1111 let boundaries = char_boundaries(line_text);
1112 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1113 if breakpoints.len() <= 2 {
1114 return None;
1115 }
1116
1117 let node_count = breakpoints.len();
1118 let mut best_cost = vec![f32::INFINITY; node_count];
1119 let mut next_index = vec![None; node_count];
1120 best_cost[node_count - 1] = 0.0;
1121
1122 for start in (0..node_count - 1).rev() {
1123 for end in start + 1..node_count {
1124 let start_byte = boundaries[breakpoints[start]];
1125 let end_byte = boundaries[breakpoints[end]];
1126 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1127 if trimmed_end <= start_byte {
1128 continue;
1129 }
1130 let segment_range = absolute_range(&line_range, start_byte..trimmed_end);
1131 let segment_width = measurer
1132 .measure_subsequence(text, segment_range, style)
1133 .width;
1134 if segment_width > max_width + WRAP_EPSILON {
1135 continue;
1136 }
1137 if !best_cost[end].is_finite() {
1138 continue;
1139 }
1140 let slack = (max_width - segment_width).max(0.0);
1141 let is_last = end == node_count - 1;
1142 let segment_cost = match line_break {
1143 LineBreak::Heading => slack * slack,
1144 LineBreak::Paragraph => {
1145 if is_last {
1146 slack * slack * 0.16
1147 } else {
1148 slack * slack
1149 }
1150 }
1151 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1152 };
1153 let candidate = segment_cost + best_cost[end];
1154 if candidate < best_cost[start] {
1155 best_cost[start] = candidate;
1156 next_index[start] = Some(end);
1157 }
1158 }
1159 }
1160
1161 let mut wrapped = Vec::new();
1162 let mut current = 0usize;
1163 while current < node_count - 1 {
1164 let next = next_index[current]?;
1165 let start_byte = boundaries[breakpoints[current]];
1166 let end_byte = boundaries[breakpoints[next]];
1167 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1168 if trimmed_end <= start_byte {
1169 return None;
1170 }
1171 wrapped.push(absolute_range(&line_range, start_byte..trimmed_end));
1172 current = next;
1173 }
1174
1175 if wrapped.is_empty() {
1176 return None;
1177 }
1178
1179 Some(wrapped)
1180}
1181
1182fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1183 let mut points = vec![0usize];
1184 for idx in 1..boundaries.len() - 1 {
1185 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1186 let current = &line[boundaries[idx]..boundaries[idx + 1]];
1187 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1188 points.push(idx);
1189 }
1190 }
1191 let end = boundaries.len() - 1;
1192 if points.last().copied() != Some(end) {
1193 points.push(end);
1194 }
1195 points
1196}
1197
1198fn choose_wrap_break(
1199 line: &str,
1200 boundaries: &[usize],
1201 start_idx: usize,
1202 best: usize,
1203 _line_break: LineBreak,
1204) -> usize {
1205 if best >= boundaries.len() - 1 {
1206 return best;
1207 }
1208
1209 if best <= start_idx + 1 {
1210 return best;
1211 }
1212
1213 for idx in (start_idx + 1..best).rev() {
1214 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1215 if prev.chars().all(char::is_whitespace) {
1216 return idx;
1217 }
1218 }
1219 best
1220}
1221
1222fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1223 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1224 return false;
1225 }
1226 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1227 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1228 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1229}
1230
1231fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1232 measurer: &M,
1233 line: &str,
1234 style: &TextStyle,
1235 boundaries: &[usize],
1236 start_idx: usize,
1237 break_idx: usize,
1238) -> usize {
1239 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx) {
1240 if is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate) {
1241 return candidate;
1242 }
1243 }
1244 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1245}
1246
1247fn is_valid_auto_hyphen_break(
1248 line: &str,
1249 boundaries: &[usize],
1250 start_idx: usize,
1251 break_idx: usize,
1252 candidate_idx: usize,
1253) -> bool {
1254 let end_idx = boundaries.len().saturating_sub(1);
1255 candidate_idx > start_idx
1256 && candidate_idx < end_idx
1257 && candidate_idx <= break_idx
1258 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1259 && is_break_inside_word(line, boundaries, candidate_idx)
1260}
1261
1262fn choose_auto_hyphen_break_fallback(
1263 boundaries: &[usize],
1264 start_idx: usize,
1265 break_idx: usize,
1266) -> usize {
1267 let end_idx = boundaries.len().saturating_sub(1);
1268 if break_idx >= end_idx {
1269 return break_idx;
1270 }
1271 let trailing_len = end_idx.saturating_sub(break_idx);
1272 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1273 return break_idx;
1274 }
1275
1276 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1277 let max_break = break_idx.saturating_sub(1);
1278 if min_break > max_break {
1279 return break_idx;
1280 }
1281
1282 let mut best_break = break_idx;
1283 let mut best_penalty = usize::MAX;
1284 for idx in min_break..=max_break {
1285 let candidate_trailing_len = end_idx.saturating_sub(idx);
1286 let candidate_prefix_len = idx.saturating_sub(start_idx);
1287 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1288 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1289 {
1290 continue;
1291 }
1292
1293 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1294 if penalty < best_penalty {
1295 best_penalty = penalty;
1296 best_break = idx;
1297 if penalty == 0 {
1298 break;
1299 }
1300 }
1301 }
1302 best_break
1303}
1304
1305fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
1306 while idx < boundaries.len() - 1 {
1307 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
1308 if !ch.chars().all(char::is_whitespace) {
1309 break;
1310 }
1311 idx += 1;
1312 }
1313 idx
1314}
1315
1316fn apply_line_overflow<M: TextMeasurer + ?Sized>(
1317 measurer: &M,
1318 line: &str,
1319 style: &TextStyle,
1320 max_width: Option<f32>,
1321 options: TextLayoutOptions,
1322 is_last_visible_line: bool,
1323 single_line_ellipsis: bool,
1324) -> String {
1325 if options.overflow == TextOverflow::Clip || !is_last_visible_line {
1326 return line.to_string();
1327 }
1328
1329 let Some(width_limit) = max_width else {
1330 return match options.overflow {
1331 TextOverflow::Ellipsis => format!("{line}{ELLIPSIS}"),
1332 TextOverflow::StartEllipsis => format!("{ELLIPSIS}{line}"),
1333 TextOverflow::MiddleEllipsis => format!("{ELLIPSIS}{line}"),
1334 TextOverflow::Clip | TextOverflow::Visible | TextOverflow::ScaleDown { .. } => {
1335 line.to_string()
1336 }
1337 };
1338 };
1339
1340 match options.overflow {
1341 TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
1342 TextOverflow::Ellipsis => fit_end_ellipsis(measurer, line, style, width_limit),
1343 TextOverflow::StartEllipsis => {
1344 if single_line_ellipsis {
1345 fit_start_ellipsis(measurer, line, style, width_limit)
1346 } else {
1347 line.to_string()
1348 }
1349 }
1350 TextOverflow::MiddleEllipsis => {
1351 if single_line_ellipsis {
1352 fit_middle_ellipsis(measurer, line, style, width_limit)
1353 } else {
1354 line.to_string()
1355 }
1356 }
1357 TextOverflow::ScaleDown { .. } => line.to_string(),
1358 }
1359}
1360
1361fn fit_end_ellipsis<M: TextMeasurer + ?Sized>(
1362 measurer: &M,
1363 line: &str,
1364 style: &TextStyle,
1365 max_width: f32,
1366) -> String {
1367 if measurer
1368 .measure(&crate::text::AnnotatedString::from(line), style)
1369 .width
1370 <= max_width + WRAP_EPSILON
1371 {
1372 return line.to_string();
1373 }
1374
1375 let ellipsis_width = measurer
1376 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1377 .width;
1378 if ellipsis_width > max_width + WRAP_EPSILON {
1379 return String::new();
1380 }
1381
1382 let boundaries = char_boundaries(line);
1383 let mut low = 0usize;
1384 let mut high = boundaries.len() - 1;
1385 let mut best = 0usize;
1386
1387 while low <= high {
1388 let mid = (low + high) / 2;
1389 let prefix = &line[..boundaries[mid]];
1390 let candidate = format!("{prefix}{ELLIPSIS}");
1391 let width = measurer
1392 .measure(
1393 &crate::text::AnnotatedString::from(candidate.as_str()),
1394 style,
1395 )
1396 .width;
1397 if width <= max_width + WRAP_EPSILON {
1398 best = mid;
1399 low = mid + 1;
1400 } else if mid == 0 {
1401 break;
1402 } else {
1403 high = mid - 1;
1404 }
1405 }
1406
1407 format!("{}{}", &line[..boundaries[best]], ELLIPSIS)
1408}
1409
1410fn fit_start_ellipsis<M: TextMeasurer + ?Sized>(
1411 measurer: &M,
1412 line: &str,
1413 style: &TextStyle,
1414 max_width: f32,
1415) -> String {
1416 if measurer
1417 .measure(&crate::text::AnnotatedString::from(line), style)
1418 .width
1419 <= max_width + WRAP_EPSILON
1420 {
1421 return line.to_string();
1422 }
1423
1424 let ellipsis_width = measurer
1425 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1426 .width;
1427 if ellipsis_width > max_width + WRAP_EPSILON {
1428 return String::new();
1429 }
1430
1431 let boundaries = char_boundaries(line);
1432 let mut low = 0usize;
1433 let mut high = boundaries.len() - 1;
1434 let mut best = boundaries.len() - 1;
1435
1436 while low <= high {
1437 let mid = (low + high) / 2;
1438 let suffix = &line[boundaries[mid]..];
1439 let candidate = format!("{ELLIPSIS}{suffix}");
1440 let width = measurer
1441 .measure(
1442 &crate::text::AnnotatedString::from(candidate.as_str()),
1443 style,
1444 )
1445 .width;
1446 if width <= max_width + WRAP_EPSILON {
1447 best = mid;
1448 if mid == 0 {
1449 break;
1450 }
1451 high = mid - 1;
1452 } else {
1453 low = mid + 1;
1454 }
1455 }
1456
1457 format!("{ELLIPSIS}{}", &line[boundaries[best]..])
1458}
1459
1460fn fit_middle_ellipsis<M: TextMeasurer + ?Sized>(
1461 measurer: &M,
1462 line: &str,
1463 style: &TextStyle,
1464 max_width: f32,
1465) -> String {
1466 if measurer
1467 .measure(&crate::text::AnnotatedString::from(line), style)
1468 .width
1469 <= max_width + WRAP_EPSILON
1470 {
1471 return line.to_string();
1472 }
1473
1474 let ellipsis_width = measurer
1475 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1476 .width;
1477 if ellipsis_width > max_width + WRAP_EPSILON {
1478 return String::new();
1479 }
1480
1481 let boundaries = char_boundaries(line);
1482 let total_chars = boundaries.len().saturating_sub(1);
1483 for keep in (0..=total_chars).rev() {
1484 let keep_start = keep.div_ceil(2);
1485 let keep_end = keep / 2;
1486 let start = &line[..boundaries[keep_start]];
1487 let end_start = boundaries[total_chars.saturating_sub(keep_end)];
1488 let end = &line[end_start..];
1489 let candidate = format!("{start}{ELLIPSIS}{end}");
1490 if measurer
1491 .measure(
1492 &crate::text::AnnotatedString::from(candidate.as_str()),
1493 style,
1494 )
1495 .width
1496 <= max_width + WRAP_EPSILON
1497 {
1498 return candidate;
1499 }
1500 }
1501
1502 ELLIPSIS.to_string()
1503}
1504
1505fn char_boundaries(text: &str) -> Vec<usize> {
1506 let mut out = Vec::with_capacity(text.chars().count() + 1);
1507 out.push(0);
1508 for (idx, _) in text.char_indices() {
1509 if idx != 0 {
1510 out.push(idx);
1511 }
1512 }
1513 out.push(text.len());
1514 out
1515}
1516
1517#[cfg(test)]
1518mod tests {
1519 use super::*;
1520 use crate::text::{Hyphens, LineBreak, ParagraphStyle, TextUnit};
1521 use crate::text_layout_result::TextLayoutResult;
1522
1523 struct ContractBreakMeasurer {
1524 retreat: usize,
1525 }
1526
1527 impl TextMeasurer for ContractBreakMeasurer {
1528 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
1529 MonospacedTextMeasurer.measure(
1530 &crate::text::AnnotatedString::from(text.text.as_str()),
1531 style,
1532 )
1533 }
1534
1535 fn get_offset_for_position(
1536 &self,
1537 text: &crate::text::AnnotatedString,
1538 style: &TextStyle,
1539 x: f32,
1540 y: f32,
1541 ) -> usize {
1542 MonospacedTextMeasurer.get_offset_for_position(
1543 &crate::text::AnnotatedString::from(text.text.as_str()),
1544 style,
1545 x,
1546 y,
1547 )
1548 }
1549
1550 fn get_cursor_x_for_offset(
1551 &self,
1552 text: &crate::text::AnnotatedString,
1553 style: &TextStyle,
1554 offset: usize,
1555 ) -> f32 {
1556 MonospacedTextMeasurer.get_cursor_x_for_offset(
1557 &crate::text::AnnotatedString::from(text.text.as_str()),
1558 style,
1559 offset,
1560 )
1561 }
1562
1563 fn layout(
1564 &self,
1565 text: &crate::text::AnnotatedString,
1566 style: &TextStyle,
1567 ) -> TextLayoutResult {
1568 MonospacedTextMeasurer.layout(
1569 &crate::text::AnnotatedString::from(text.text.as_str()),
1570 style,
1571 )
1572 }
1573
1574 fn choose_auto_hyphen_break(
1575 &self,
1576 _line: &str,
1577 _style: &TextStyle,
1578 _segment_start_char: usize,
1579 measured_break_char: usize,
1580 ) -> Option<usize> {
1581 measured_break_char.checked_sub(self.retreat)
1582 }
1583 }
1584
1585 fn style_with_line_break(line_break: LineBreak) -> TextStyle {
1586 TextStyle {
1587 span_style: crate::text::SpanStyle {
1588 font_size: TextUnit::Sp(10.0),
1589 ..Default::default()
1590 },
1591 paragraph_style: ParagraphStyle {
1592 line_break,
1593 ..Default::default()
1594 },
1595 }
1596 }
1597
1598 fn style_with_hyphens(hyphens: Hyphens) -> TextStyle {
1599 TextStyle {
1600 span_style: crate::text::SpanStyle {
1601 font_size: TextUnit::Sp(10.0),
1602 ..Default::default()
1603 },
1604 paragraph_style: ParagraphStyle {
1605 hyphens,
1606 ..Default::default()
1607 },
1608 }
1609 }
1610
1611 fn assert_f32_close(actual: f32, expected: f32) {
1612 assert!(
1613 (actual - expected).abs() <= 0.01,
1614 "actual={actual}, expected={expected}"
1615 );
1616 }
1617
1618 #[test]
1619 fn text_layout_options_wraps_and_limits_lines() {
1620 let style = TextStyle {
1621 span_style: crate::text::SpanStyle {
1622 font_size: TextUnit::Sp(10.0),
1623 ..Default::default()
1624 },
1625 ..Default::default()
1626 };
1627 let options = TextLayoutOptions {
1628 overflow: TextOverflow::Clip,
1629 soft_wrap: true,
1630 max_lines: 2,
1631 min_lines: 1,
1632 };
1633
1634 let prepared = prepare_text_layout(
1635 &crate::text::AnnotatedString::from("A B C D E F"),
1636 &style,
1637 options,
1638 Some(24.0), );
1640
1641 assert!(prepared.did_overflow);
1642 assert!(prepared.metrics.line_count <= 2);
1643 }
1644
1645 #[test]
1646 fn text_layout_options_end_ellipsis_applies() {
1647 let style = TextStyle {
1648 span_style: crate::text::SpanStyle {
1649 font_size: TextUnit::Sp(10.0),
1650 ..Default::default()
1651 },
1652 ..Default::default()
1653 };
1654 let options = TextLayoutOptions {
1655 overflow: TextOverflow::Ellipsis,
1656 soft_wrap: false,
1657 max_lines: 1,
1658 min_lines: 1,
1659 };
1660
1661 let prepared = prepare_text_layout(
1662 &crate::text::AnnotatedString::from("Long long line"),
1663 &style,
1664 options,
1665 Some(20.0),
1666 );
1667 assert!(prepared.did_overflow);
1668 assert!(prepared.text.text.contains(ELLIPSIS));
1669 }
1670
1671 #[test]
1672 fn text_layout_options_visible_keeps_full_text() {
1673 let style = TextStyle {
1674 span_style: crate::text::SpanStyle {
1675 font_size: TextUnit::Sp(10.0),
1676 ..Default::default()
1677 },
1678 ..Default::default()
1679 };
1680 let options = TextLayoutOptions {
1681 overflow: TextOverflow::Visible,
1682 soft_wrap: false,
1683 max_lines: 1,
1684 min_lines: 1,
1685 };
1686
1687 let input = "This should remain unchanged";
1688 let prepared = prepare_text_layout(
1689 &crate::text::AnnotatedString::from(input),
1690 &style,
1691 options,
1692 Some(10.0),
1693 );
1694 assert_eq!(prepared.text.text, input);
1695 }
1696
1697 #[test]
1698 fn text_layout_options_respects_min_lines() {
1699 let style = TextStyle {
1700 span_style: crate::text::SpanStyle {
1701 font_size: TextUnit::Sp(10.0),
1702 ..Default::default()
1703 },
1704 ..Default::default()
1705 };
1706 let options = TextLayoutOptions {
1707 overflow: TextOverflow::Clip,
1708 soft_wrap: true,
1709 max_lines: 4,
1710 min_lines: 3,
1711 };
1712
1713 let prepared = prepare_text_layout(
1714 &crate::text::AnnotatedString::from("short"),
1715 &style,
1716 options,
1717 Some(100.0),
1718 );
1719 assert_eq!(prepared.metrics.line_count, 3);
1720 }
1721
1722 #[test]
1723 fn text_layout_options_middle_ellipsis_for_single_line() {
1724 let style = TextStyle {
1725 span_style: crate::text::SpanStyle {
1726 font_size: TextUnit::Sp(10.0),
1727 ..Default::default()
1728 },
1729 ..Default::default()
1730 };
1731 let options = TextLayoutOptions {
1732 overflow: TextOverflow::MiddleEllipsis,
1733 soft_wrap: false,
1734 max_lines: 1,
1735 min_lines: 1,
1736 };
1737
1738 let prepared = prepare_text_layout(
1739 &crate::text::AnnotatedString::from("abcdefghijk"),
1740 &style,
1741 options,
1742 Some(24.0),
1743 );
1744 assert!(prepared.text.text.contains(ELLIPSIS));
1745 assert!(prepared.did_overflow);
1746 }
1747
1748 #[test]
1749 fn text_layout_options_scale_down_fits_without_rewriting_text() {
1750 let style = TextStyle {
1751 span_style: crate::text::SpanStyle {
1752 font_size: TextUnit::Sp(20.0),
1753 ..Default::default()
1754 },
1755 ..Default::default()
1756 };
1757 let options = TextLayoutOptions {
1758 overflow: TextOverflow::ScaleDown {
1759 min_font_size_sp: 10.0,
1760 },
1761 soft_wrap: false,
1762 max_lines: 1,
1763 min_lines: 1,
1764 };
1765
1766 let prepared = prepare_text_layout(
1767 &crate::text::AnnotatedString::from("ABCDE"),
1768 &style,
1769 options,
1770 Some(36.0),
1771 );
1772
1773 assert_eq!(prepared.text.text, "ABCDE");
1774 assert!(prepared.metrics.width <= 36.0 + WRAP_EPSILON);
1775 assert!(!prepared.did_overflow);
1776 let visual_font_size = prepared.visual_style.resolve_font_size(14.0);
1777 assert!(visual_font_size < 20.0);
1778 assert!(visual_font_size >= 10.0);
1779 }
1780
1781 #[test]
1782 fn text_layout_options_scale_down_scales_root_shadow() {
1783 let style = TextStyle {
1784 span_style: crate::text::SpanStyle {
1785 font_size: TextUnit::Sp(20.0),
1786 shadow: Some(crate::text::Shadow {
1787 color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
1788 offset: crate::modifier::Point::new(8.0, 4.0),
1789 blur_radius: 6.0,
1790 }),
1791 ..Default::default()
1792 },
1793 ..Default::default()
1794 };
1795 let options = TextLayoutOptions {
1796 overflow: TextOverflow::ScaleDown {
1797 min_font_size_sp: 10.0,
1798 },
1799 soft_wrap: false,
1800 max_lines: 1,
1801 min_lines: 1,
1802 };
1803
1804 let prepared = prepare_text_layout(
1805 &crate::text::AnnotatedString::from("ABCDE"),
1806 &style,
1807 options,
1808 Some(36.0),
1809 );
1810
1811 let font_scale = prepared.visual_style.resolve_font_size(14.0) / 20.0;
1812 let shadow = prepared
1813 .visual_style
1814 .span_style
1815 .shadow
1816 .expect("scaled style should retain shadow");
1817 assert_f32_close(shadow.offset.x, 8.0 * font_scale);
1818 assert_f32_close(shadow.offset.y, 4.0 * font_scale);
1819 assert_f32_close(shadow.blur_radius, 6.0 * font_scale);
1820 }
1821
1822 #[test]
1823 fn text_layout_options_scale_down_stops_at_minimum_and_clips() {
1824 let style = TextStyle {
1825 span_style: crate::text::SpanStyle {
1826 font_size: TextUnit::Sp(20.0),
1827 ..Default::default()
1828 },
1829 ..Default::default()
1830 };
1831 let options = TextLayoutOptions {
1832 overflow: TextOverflow::ScaleDown {
1833 min_font_size_sp: 10.0,
1834 },
1835 soft_wrap: false,
1836 max_lines: 1,
1837 min_lines: 1,
1838 };
1839
1840 let prepared = prepare_text_layout(
1841 &crate::text::AnnotatedString::from("ABCDEFGHIJ"),
1842 &style,
1843 options,
1844 Some(12.0),
1845 );
1846
1847 assert_eq!(prepared.text.text, "ABCDEFGHIJ");
1848 assert!(prepared.did_overflow);
1849 assert_eq!(prepared.metrics.width, 12.0);
1850 assert_eq!(prepared.visual_style.resolve_font_size(14.0), 10.0);
1851 }
1852
1853 #[test]
1854 fn scale_annotated_font_sizes_borrows_when_spans_need_no_scaling() {
1855 let plain = crate::text::AnnotatedString::from("plain");
1856 assert!(matches!(
1857 scale_annotated_font_sizes(&plain, 0.5),
1858 std::borrow::Cow::Borrowed(_)
1859 ));
1860
1861 let colored = crate::text::annotated_string::Builder::new()
1862 .push_style(crate::text::SpanStyle {
1863 color: Some(crate::modifier::Color(1.0, 0.0, 0.0, 1.0)),
1864 ..Default::default()
1865 })
1866 .append("colored")
1867 .pop()
1868 .to_annotated_string();
1869 assert!(matches!(
1870 scale_annotated_font_sizes(&colored, 0.5),
1871 std::borrow::Cow::Borrowed(_)
1872 ));
1873 }
1874
1875 #[test]
1876 fn scale_annotated_font_sizes_scales_span_shadow_geometry() {
1877 let text = crate::text::annotated_string::Builder::new()
1878 .push_style(crate::text::SpanStyle {
1879 shadow: Some(crate::text::Shadow {
1880 color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
1881 offset: crate::modifier::Point::new(6.0, 2.0),
1882 blur_radius: 4.0,
1883 }),
1884 ..Default::default()
1885 })
1886 .append("shadow")
1887 .pop()
1888 .to_annotated_string();
1889
1890 let scaled = scale_annotated_font_sizes(&text, 0.5);
1891 let std::borrow::Cow::Owned(scaled) = scaled else {
1892 panic!("shadowed span should be scaled into owned text");
1893 };
1894 let shadow = scaled.span_styles[0]
1895 .item
1896 .shadow
1897 .expect("scaled span should retain shadow");
1898 assert_f32_close(shadow.offset.x, 3.0);
1899 assert_f32_close(shadow.offset.y, 1.0);
1900 assert_f32_close(shadow.blur_radius, 2.0);
1901 }
1902
1903 #[test]
1904 fn text_layout_options_does_not_wrap_on_tiny_width_delta() {
1905 let style = TextStyle {
1906 span_style: crate::text::SpanStyle {
1907 font_size: TextUnit::Sp(10.0),
1908 ..Default::default()
1909 },
1910 ..Default::default()
1911 };
1912 let options = TextLayoutOptions {
1913 overflow: TextOverflow::Clip,
1914 soft_wrap: true,
1915 max_lines: usize::MAX,
1916 min_lines: 1,
1917 };
1918
1919 let text = "if counter % 2 == 0";
1920 let exact_width = measure_text(&crate::text::AnnotatedString::from(text), &style).width;
1921 let prepared = prepare_text_layout(
1922 &crate::text::AnnotatedString::from(text),
1923 &style,
1924 options,
1925 Some(exact_width - 0.1),
1926 );
1927
1928 assert!(
1929 !prepared.text.text.contains('\n'),
1930 "unexpected line split: {:?}",
1931 prepared.text
1932 );
1933 }
1934
1935 #[test]
1936 fn line_break_mode_changes_wrap_strategy_contract() {
1937 let text = "This is an example text";
1938 let options = TextLayoutOptions {
1939 overflow: TextOverflow::Clip,
1940 soft_wrap: true,
1941 max_lines: usize::MAX,
1942 min_lines: 1,
1943 };
1944
1945 let simple = prepare_text_layout(
1946 &crate::text::AnnotatedString::from(text),
1947 &style_with_line_break(LineBreak::Simple),
1948 options,
1949 Some(120.0),
1950 );
1951 let heading = prepare_text_layout(
1952 &crate::text::AnnotatedString::from(text),
1953 &style_with_line_break(LineBreak::Heading),
1954 options,
1955 Some(120.0),
1956 );
1957 let paragraph = prepare_text_layout(
1958 &crate::text::AnnotatedString::from(text),
1959 &style_with_line_break(LineBreak::Paragraph),
1960 options,
1961 Some(50.0),
1962 );
1963
1964 assert_eq!(
1965 simple.text.text.lines().collect::<Vec<_>>(),
1966 vec!["This is an example", "text"]
1967 );
1968 assert_eq!(
1969 heading.text.text.lines().collect::<Vec<_>>(),
1970 vec!["This is an", "example text"]
1971 );
1972 assert_eq!(
1973 paragraph.text.text.lines().collect::<Vec<_>>(),
1974 vec!["This", "is an", "example", "text"]
1975 );
1976 }
1977
1978 #[test]
1979 fn hyphens_mode_changes_wrap_strategy_contract() {
1980 let text = "Transformation";
1981 let options = TextLayoutOptions {
1982 overflow: TextOverflow::Clip,
1983 soft_wrap: true,
1984 max_lines: usize::MAX,
1985 min_lines: 1,
1986 };
1987
1988 let auto = prepare_text_layout(
1989 &crate::text::AnnotatedString::from(text),
1990 &style_with_hyphens(Hyphens::Auto),
1991 options,
1992 Some(24.0),
1993 );
1994 let none = prepare_text_layout(
1995 &crate::text::AnnotatedString::from(text),
1996 &style_with_hyphens(Hyphens::None),
1997 options,
1998 Some(24.0),
1999 );
2000
2001 assert_eq!(
2002 auto.text.text.lines().collect::<Vec<_>>(),
2003 vec!["Tran", "sfor", "ma", "tion"]
2004 );
2005 assert_eq!(
2006 none.text.text.lines().collect::<Vec<_>>(),
2007 vec!["Tran", "sfor", "mati", "on"]
2008 );
2009 assert!(
2010 !auto.text.text.contains('-'),
2011 "automatic hyphenation should influence breaks without mutating source text content"
2012 );
2013 }
2014
2015 #[test]
2016 fn hyphens_auto_uses_measurer_hyphen_contract_when_valid() {
2017 let text = "Transformation";
2018 let style = style_with_hyphens(Hyphens::Auto);
2019 let options = TextLayoutOptions {
2020 overflow: TextOverflow::Clip,
2021 soft_wrap: true,
2022 max_lines: usize::MAX,
2023 min_lines: 1,
2024 };
2025
2026 let prepared = prepare_text_layout_fallback(
2027 &ContractBreakMeasurer { retreat: 1 },
2028 &crate::text::AnnotatedString::from(text),
2029 &style,
2030 options,
2031 Some(24.0),
2032 );
2033
2034 assert_eq!(
2035 prepared.text.text.lines().collect::<Vec<_>>(),
2036 vec!["Tra", "nsf", "orm", "ati", "on"]
2037 );
2038 }
2039
2040 #[test]
2041 fn hyphens_auto_falls_back_when_measurer_hyphen_contract_is_invalid() {
2042 let text = "Transformation";
2043 let style = style_with_hyphens(Hyphens::Auto);
2044 let options = TextLayoutOptions {
2045 overflow: TextOverflow::Clip,
2046 soft_wrap: true,
2047 max_lines: usize::MAX,
2048 min_lines: 1,
2049 };
2050
2051 let prepared = prepare_text_layout_fallback(
2052 &ContractBreakMeasurer { retreat: 10 },
2053 &crate::text::AnnotatedString::from(text),
2054 &style,
2055 options,
2056 Some(24.0),
2057 );
2058
2059 assert_eq!(
2060 prepared.text.text.lines().collect::<Vec<_>>(),
2061 vec!["Tran", "sfor", "ma", "tion"]
2062 );
2063 }
2064
2065 #[test]
2066 fn transformed_text_keeps_span_ranges_within_display_bounds() {
2067 let style = TextStyle {
2068 span_style: crate::text::SpanStyle {
2069 font_size: TextUnit::Sp(10.0),
2070 ..Default::default()
2071 },
2072 ..Default::default()
2073 };
2074 let options = TextLayoutOptions {
2075 overflow: TextOverflow::Ellipsis,
2076 soft_wrap: false,
2077 max_lines: 1,
2078 min_lines: 1,
2079 };
2080 let annotated = crate::text::AnnotatedString::builder()
2081 .push_style(crate::text::SpanStyle {
2082 font_weight: Some(crate::text::FontWeight::BOLD),
2083 ..Default::default()
2084 })
2085 .append("Styled overflow text sample")
2086 .pop()
2087 .to_annotated_string();
2088
2089 let prepared = prepare_text_layout(&annotated, &style, options, Some(40.0));
2090 assert!(prepared.did_overflow);
2091 for span in &prepared.text.span_styles {
2092 assert!(span.range.start < span.range.end);
2093 assert!(span.range.end <= prepared.text.text.len());
2094 assert!(prepared.text.text.is_char_boundary(span.range.start));
2095 assert!(prepared.text.text.is_char_boundary(span.range.end));
2096 }
2097 }
2098
2099 #[test]
2100 fn wrapped_text_splits_styles_around_inserted_newlines() {
2101 let style = TextStyle {
2102 span_style: crate::text::SpanStyle {
2103 font_size: TextUnit::Sp(10.0),
2104 ..Default::default()
2105 },
2106 ..Default::default()
2107 };
2108 let options = TextLayoutOptions {
2109 overflow: TextOverflow::Clip,
2110 soft_wrap: true,
2111 max_lines: usize::MAX,
2112 min_lines: 1,
2113 };
2114 let annotated = crate::text::AnnotatedString::builder()
2115 .push_style(crate::text::SpanStyle {
2116 text_decoration: Some(crate::text::TextDecoration::UNDERLINE),
2117 ..Default::default()
2118 })
2119 .append("Wrapped style text example")
2120 .pop()
2121 .to_annotated_string();
2122
2123 let prepared = prepare_text_layout(&annotated, &style, options, Some(32.0));
2124 assert!(prepared.text.text.contains('\n'));
2125 assert!(!prepared.text.span_styles.is_empty());
2126 for span in &prepared.text.span_styles {
2127 assert!(span.range.end <= prepared.text.text.len());
2128 }
2129 }
2130
2131 #[test]
2132 fn mixed_font_size_segments_wrap_without_truncation() {
2133 let style = TextStyle {
2134 span_style: crate::text::SpanStyle {
2135 font_size: TextUnit::Sp(14.0),
2136 ..Default::default()
2137 },
2138 ..Default::default()
2139 };
2140 let options = TextLayoutOptions {
2141 overflow: TextOverflow::Clip,
2142 soft_wrap: true,
2143 max_lines: usize::MAX,
2144 min_lines: 1,
2145 };
2146 let annotated = crate::text::AnnotatedString::builder()
2147 .append("You can also ")
2148 .push_style(crate::text::SpanStyle {
2149 font_size: TextUnit::Sp(22.0),
2150 ..Default::default()
2151 })
2152 .append("change font size")
2153 .pop()
2154 .append(" dynamically mid-sentence!")
2155 .to_annotated_string();
2156
2157 let prepared = prepare_text_layout(&annotated, &style, options, Some(260.0));
2158 assert!(prepared.text.text.contains('\n'));
2159 assert!(prepared.text.text.contains("mid-sentence!"));
2160 assert!(!prepared.did_overflow);
2161 }
2162}