1use crate::text_layout_result::TextLayoutResult;
2use std::cell::RefCell;
3
4use super::layout_options::{TextLayoutOptions, TextOverflow};
5use super::paragraph::{Hyphens, LineBreak};
6use super::style::TextStyle;
7
8const ELLIPSIS: &str = "\u{2026}";
9const WRAP_EPSILON: f32 = 0.5;
10const AUTO_HYPHEN_MIN_SEGMENT_CHARS: usize = 2;
11const AUTO_HYPHEN_MIN_TRAILING_CHARS: usize = 3;
12const AUTO_HYPHEN_PREFERRED_TRAILING_CHARS: usize = 4;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct TextMetrics {
16 pub width: f32,
17 pub height: f32,
18 pub line_height: f32,
20 pub line_count: usize,
22}
23
24#[derive(Clone, Debug, PartialEq)]
25pub struct PreparedTextLayout {
26 pub text: crate::text::AnnotatedString,
27 pub metrics: TextMetrics,
28 pub did_overflow: bool,
29}
30
31pub trait TextMeasurer: 'static {
32 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
33
34 fn get_offset_for_position(
35 &self,
36 text: &crate::text::AnnotatedString,
37 style: &TextStyle,
38 x: f32,
39 y: f32,
40 ) -> usize;
41
42 fn get_cursor_x_for_offset(
43 &self,
44 text: &crate::text::AnnotatedString,
45 style: &TextStyle,
46 offset: usize,
47 ) -> f32;
48
49 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
50
51 fn choose_auto_hyphen_break(
57 &self,
58 _line: &str,
59 _style: &TextStyle,
60 _segment_start_char: usize,
61 _measured_break_char: usize,
62 ) -> Option<usize> {
63 None
64 }
65
66 fn measure_with_options(
67 &self,
68 text: &crate::text::AnnotatedString,
69 style: &TextStyle,
70 options: TextLayoutOptions,
71 max_width: Option<f32>,
72 ) -> TextMetrics {
73 self.prepare_with_options(text, style, options, max_width)
74 .metrics
75 }
76
77 fn prepare_with_options(
78 &self,
79 text: &crate::text::AnnotatedString,
80 style: &TextStyle,
81 options: TextLayoutOptions,
82 max_width: Option<f32>,
83 ) -> PreparedTextLayout {
84 self.prepare_with_options_fallback(text, style, options, max_width)
85 }
86
87 fn prepare_with_options_fallback(
88 &self,
89 text: &crate::text::AnnotatedString,
90 style: &TextStyle,
91 options: TextLayoutOptions,
92 max_width: Option<f32>,
93 ) -> PreparedTextLayout {
94 prepare_text_layout_fallback(self, text, style, options, max_width)
95 }
96}
97
98#[derive(Default)]
99struct MonospacedTextMeasurer;
100
101impl MonospacedTextMeasurer {
102 const DEFAULT_SIZE: f32 = 14.0;
103 const CHAR_WIDTH_RATIO: f32 = 0.6; fn get_metrics(style: &TextStyle) -> (f32, f32) {
106 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
107 let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
108 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
109 (
110 (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
111 line_height,
112 )
113 }
114}
115
116impl TextMeasurer for MonospacedTextMeasurer {
117 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
118 let (char_width, line_height) = Self::get_metrics(style);
119
120 let lines: Vec<&str> = text.text.split('\n').collect();
121 let line_count = lines.len().max(1);
122
123 let width = lines
124 .iter()
125 .map(|line| line.chars().count() as f32 * char_width)
126 .fold(0.0_f32, f32::max);
127
128 TextMetrics {
129 width,
130 height: line_count as f32 * line_height,
131 line_height,
132 line_count,
133 }
134 }
135
136 fn get_offset_for_position(
137 &self,
138 text: &crate::text::AnnotatedString,
139 style: &TextStyle,
140 x: f32,
141 y: f32,
142 ) -> usize {
143 let (char_width, line_height) = Self::get_metrics(style);
144
145 if text.text.is_empty() {
146 return 0;
147 }
148
149 let line_index = (y / line_height).floor().max(0.0) as usize;
150 let lines: Vec<&str> = text.text.split('\n').collect();
151 let target_line = line_index.min(lines.len().saturating_sub(1));
152
153 let mut line_start_byte = 0;
154 for line in lines.iter().take(target_line) {
155 line_start_byte += line.len() + 1;
156 }
157
158 let line_text = lines.get(target_line).unwrap_or(&"");
159 let char_index = (x / char_width).round() as usize;
160 let line_char_count = line_text.chars().count();
161 let clamped_index = char_index.min(line_char_count);
162
163 let offset_in_line = line_text
164 .char_indices()
165 .nth(clamped_index)
166 .map(|(i, _)| i)
167 .unwrap_or(line_text.len());
168
169 line_start_byte + offset_in_line
170 }
171
172 fn get_cursor_x_for_offset(
173 &self,
174 text: &crate::text::AnnotatedString,
175 style: &TextStyle,
176 offset: usize,
177 ) -> f32 {
178 let (char_width, _) = Self::get_metrics(style);
179
180 let clamped_offset = offset.min(text.text.len());
181 let char_count = text.text[..clamped_offset].chars().count();
182 char_count as f32 * char_width
183 }
184
185 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
186 let (char_width, line_height) = Self::get_metrics(style);
187 TextLayoutResult::monospaced(&text.text, char_width, line_height)
188 }
189}
190
191thread_local! {
192 static TEXT_MEASURER: RefCell<Box<dyn TextMeasurer>> = RefCell::new(Box::new(MonospacedTextMeasurer));
193}
194
195pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
196 TEXT_MEASURER.with(|m| {
197 *m.borrow_mut() = Box::new(measurer);
198 });
199}
200
201pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
202 TEXT_MEASURER.with(|m| m.borrow().measure(text, style))
203}
204
205pub fn measure_text_with_options(
206 text: &crate::text::AnnotatedString,
207 style: &TextStyle,
208 options: TextLayoutOptions,
209 max_width: Option<f32>,
210) -> TextMetrics {
211 TEXT_MEASURER.with(|m| {
212 m.borrow()
213 .measure_with_options(text, style, options.normalized(), max_width)
214 })
215}
216
217pub fn prepare_text_layout(
218 text: &crate::text::AnnotatedString,
219 style: &TextStyle,
220 options: TextLayoutOptions,
221 max_width: Option<f32>,
222) -> PreparedTextLayout {
223 TEXT_MEASURER.with(|m| {
224 m.borrow()
225 .prepare_with_options(text, style, options.normalized(), max_width)
226 })
227}
228
229pub fn get_offset_for_position(
230 text: &crate::text::AnnotatedString,
231 style: &TextStyle,
232 x: f32,
233 y: f32,
234) -> usize {
235 TEXT_MEASURER.with(|m| m.borrow().get_offset_for_position(text, style, x, y))
236}
237
238pub fn get_cursor_x_for_offset(
239 text: &crate::text::AnnotatedString,
240 style: &TextStyle,
241 offset: usize,
242) -> f32 {
243 TEXT_MEASURER.with(|m| m.borrow().get_cursor_x_for_offset(text, style, offset))
244}
245
246pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
247 TEXT_MEASURER.with(|m| m.borrow().layout(text, style))
248}
249
250fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
251 measurer: &M,
252 text: &crate::text::AnnotatedString,
253 style: &TextStyle,
254 options: TextLayoutOptions,
255 max_width: Option<f32>,
256) -> PreparedTextLayout {
257 let opts = options.normalized();
258 let max_width = normalize_max_width(max_width);
259 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
260 .then_some(max_width)
261 .flatten();
262 let line_break_mode = style
263 .paragraph_style
264 .line_break
265 .take_or_else(|| LineBreak::Simple);
266 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
267
268 let mut lines = split_text_lines(text.text.as_str());
269 let mut annotated_lines = split_annotated_lines(text);
270 if let Some(width_limit) = wrap_width {
271 let mut wrapped = Vec::with_capacity(lines.len());
272 let mut wrapped_annotated = Vec::with_capacity(lines.len());
273 for line in &annotated_lines {
274 let wrapped_lines = wrap_line_to_width(
275 measurer,
276 line,
277 style,
278 width_limit,
279 line_break_mode,
280 hyphens_mode,
281 );
282 for wrapped_line in wrapped_lines {
283 wrapped.push(wrapped_line.text.clone());
284 wrapped_annotated.push(wrapped_line);
285 }
286 }
287 lines = wrapped;
288 annotated_lines = wrapped_annotated;
289 }
290
291 let mut did_overflow = false;
292 let mut visible_lines = lines.clone();
293 let mut visible_annotated_lines = annotated_lines.clone();
294
295 if opts.overflow != TextOverflow::Visible && visible_lines.len() > opts.max_lines {
296 did_overflow = true;
297 visible_lines.truncate(opts.max_lines);
298 visible_annotated_lines.truncate(opts.max_lines);
299 if let Some(last_line) = visible_lines.last_mut() {
300 *last_line =
301 apply_line_overflow(measurer, last_line, style, max_width, opts, true, true);
302 if let Some(last_annotated_line) = visible_annotated_lines.last_mut() {
303 *last_annotated_line =
304 remap_annotated_for_display(last_annotated_line, last_line.as_str());
305 }
306 }
307 }
308
309 if let Some(width_limit) = max_width {
310 let single_line_ellipsis = opts.max_lines == 1 || !opts.soft_wrap;
311 let visible_len = visible_lines.len();
312 for (line_index, line) in visible_lines.iter_mut().enumerate() {
313 let width = visible_annotated_lines
314 .get(line_index)
315 .map(|annotated_line| measurer.measure(annotated_line, style).width)
316 .unwrap_or_default();
317 if width > width_limit + WRAP_EPSILON {
318 if opts.overflow == TextOverflow::Visible {
319 continue;
320 }
321 did_overflow = true;
322 *line = apply_line_overflow(
323 measurer,
324 line,
325 style,
326 Some(width_limit),
327 opts,
328 line_index + 1 == visible_len,
329 single_line_ellipsis,
330 );
331 if let Some(annotated_line) = visible_annotated_lines.get_mut(line_index) {
332 *annotated_line = remap_annotated_for_display(annotated_line, line.as_str());
333 }
334 }
335 }
336 }
337
338 let display_annotated = join_annotated_lines(&visible_annotated_lines);
339 debug_assert_eq!(display_annotated.text, visible_lines.join("\n"));
340 let line_height = measurer.measure(text, style).line_height.max(0.0);
341 let display_line_count = visible_lines.len().max(1);
342 let layout_line_count = display_line_count.max(opts.min_lines);
343
344 let measured_width = if visible_annotated_lines.is_empty() {
345 0.0
346 } else {
347 visible_annotated_lines
348 .iter()
349 .map(|line| measurer.measure(line, style).width)
350 .fold(0.0_f32, f32::max)
351 };
352 let width = if opts.overflow == TextOverflow::Visible {
353 measured_width
354 } else if let Some(width_limit) = max_width {
355 measured_width.min(width_limit)
356 } else {
357 measured_width
358 };
359
360 PreparedTextLayout {
361 text: display_annotated,
362 metrics: TextMetrics {
363 width,
364 height: layout_line_count as f32 * line_height,
365 line_height,
366 line_count: layout_line_count,
367 },
368 did_overflow,
369 }
370}
371
372fn split_text_lines(text: &str) -> Vec<String> {
373 if text.is_empty() {
374 return vec![String::new()];
375 }
376 text.split('\n').map(ToString::to_string).collect()
377}
378
379fn split_annotated_lines(text: &crate::text::AnnotatedString) -> Vec<crate::text::AnnotatedString> {
380 if text.text.is_empty() {
381 return vec![crate::text::AnnotatedString::from("")];
382 }
383
384 let mut out = Vec::new();
385 let mut start = 0usize;
386 for (idx, ch) in text.text.char_indices() {
387 if ch == '\n' {
388 out.push(text.subsequence(start..idx));
389 start = idx + ch.len_utf8();
390 }
391 }
392 out.push(text.subsequence(start..text.text.len()));
393 out
394}
395
396fn join_annotated_lines(lines: &[crate::text::AnnotatedString]) -> crate::text::AnnotatedString {
397 if lines.is_empty() {
398 return crate::text::AnnotatedString::from("");
399 }
400
401 let mut text = String::new();
402 let mut span_styles = Vec::new();
403 let mut paragraph_styles = Vec::new();
404 let mut string_annotations = Vec::new();
405 let mut link_annotations = Vec::new();
406 let mut offset = 0usize;
407
408 for (idx, line) in lines.iter().enumerate() {
409 text.push_str(line.text.as_str());
410 for span in &line.span_styles {
411 span_styles.push(crate::text::RangeStyle {
412 item: span.item.clone(),
413 range: (span.range.start + offset)..(span.range.end + offset),
414 });
415 }
416 for span in &line.paragraph_styles {
417 paragraph_styles.push(crate::text::RangeStyle {
418 item: span.item.clone(),
419 range: (span.range.start + offset)..(span.range.end + offset),
420 });
421 }
422 for ann in &line.string_annotations {
423 string_annotations.push(crate::text::RangeStyle {
424 item: ann.item.clone(),
425 range: (ann.range.start + offset)..(ann.range.end + offset),
426 });
427 }
428 for ann in &line.link_annotations {
429 link_annotations.push(crate::text::RangeStyle {
430 item: ann.item.clone(),
431 range: (ann.range.start + offset)..(ann.range.end + offset),
432 });
433 }
434
435 offset += line.text.len();
436 if idx + 1 < lines.len() {
437 text.push('\n');
438 offset += 1;
439 }
440 }
441
442 crate::text::AnnotatedString {
443 text,
444 span_styles,
445 paragraph_styles,
446 string_annotations,
447 link_annotations,
448 }
449}
450
451fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
452 while end > start {
453 let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
454 break;
455 };
456 if ch.is_whitespace() {
457 end = start + idx;
458 } else {
459 break;
460 }
461 }
462 end
463}
464
465fn remap_annotated_for_display(
466 source: &crate::text::AnnotatedString,
467 display_text: &str,
468) -> crate::text::AnnotatedString {
469 if source.text == display_text {
470 return source.clone();
471 }
472
473 let display_chars = map_display_chars_to_source(source.text.as_str(), display_text);
474 crate::text::AnnotatedString {
475 text: display_text.to_string(),
476 span_styles: remap_range_styles(&source.span_styles, &display_chars),
477 paragraph_styles: remap_range_styles(&source.paragraph_styles, &display_chars),
478 string_annotations: remap_range_styles(&source.string_annotations, &display_chars),
479 link_annotations: remap_range_styles(&source.link_annotations, &display_chars),
480 }
481}
482
483#[derive(Clone, Copy)]
484struct DisplayCharMap {
485 display_start: usize,
486 display_end: usize,
487 source_start: Option<usize>,
488}
489
490fn map_display_chars_to_source(source: &str, display: &str) -> Vec<DisplayCharMap> {
491 let source_chars: Vec<(usize, char)> = source.char_indices().collect();
492 let mut source_index = 0usize;
493 let mut maps = Vec::with_capacity(display.chars().count());
494
495 for (display_start, display_char) in display.char_indices() {
496 let display_end = display_start + display_char.len_utf8();
497 let mut source_start = None;
498 while source_index < source_chars.len() {
499 let (candidate_start, candidate_char) = source_chars[source_index];
500 source_index += 1;
501 if candidate_char == display_char {
502 source_start = Some(candidate_start);
503 break;
504 }
505 }
506 maps.push(DisplayCharMap {
507 display_start,
508 display_end,
509 source_start,
510 });
511 }
512
513 maps
514}
515
516fn remap_range_styles<T: Clone>(
517 styles: &[crate::text::RangeStyle<T>],
518 display_chars: &[DisplayCharMap],
519) -> Vec<crate::text::RangeStyle<T>> {
520 let mut remapped = Vec::new();
521
522 for style in styles {
523 let mut range_start = None;
524 let mut range_end = 0usize;
525
526 for map in display_chars {
527 let in_range = map.source_start.is_some_and(|source_start| {
528 source_start >= style.range.start && source_start < style.range.end
529 });
530
531 if in_range {
532 if range_start.is_none() {
533 range_start = Some(map.display_start);
534 }
535 range_end = map.display_end;
536 continue;
537 }
538
539 if let Some(start) = range_start.take() {
540 if start < range_end {
541 remapped.push(crate::text::RangeStyle {
542 item: style.item.clone(),
543 range: start..range_end,
544 });
545 }
546 }
547 }
548
549 if let Some(start) = range_start.take() {
550 if start < range_end {
551 remapped.push(crate::text::RangeStyle {
552 item: style.item.clone(),
553 range: start..range_end,
554 });
555 }
556 }
557 }
558
559 remapped
560}
561
562fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
563 match max_width {
564 Some(width) if width.is_finite() && width > 0.0 => Some(width),
565 _ => None,
566 }
567}
568
569fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
570 measurer: &M,
571 line: &crate::text::AnnotatedString,
572 style: &TextStyle,
573 max_width: f32,
574 line_break: LineBreak,
575 hyphens: Hyphens,
576) -> Vec<crate::text::AnnotatedString> {
577 if line.text.is_empty() {
578 return vec![crate::text::AnnotatedString::from("")];
579 }
580
581 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
582 && line.text.chars().any(char::is_whitespace)
583 {
584 if let Some(balanced) =
585 wrap_line_with_word_balance(measurer, line, style, max_width, line_break)
586 {
587 return balanced;
588 }
589 }
590
591 wrap_line_greedy(measurer, line, style, max_width, line_break, hyphens)
592}
593
594fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
595 measurer: &M,
596 line: &crate::text::AnnotatedString,
597 style: &TextStyle,
598 max_width: f32,
599 line_break: LineBreak,
600 hyphens: Hyphens,
601) -> Vec<crate::text::AnnotatedString> {
602 let line_text = line.text.as_str();
603 let boundaries = char_boundaries(line_text);
604 let mut wrapped = Vec::new();
605 let mut start_idx = 0usize;
606
607 while start_idx < boundaries.len() - 1 {
608 let mut low = start_idx + 1;
609 let mut high = boundaries.len() - 1;
610 let mut best = start_idx + 1;
611
612 while low <= high {
613 let mid = (low + high) / 2;
614 let segment = line.subsequence(boundaries[start_idx]..boundaries[mid]);
615 let width = measurer.measure(&segment, style).width;
616 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
617 best = mid;
618 low = mid + 1;
619 } else {
620 if mid == 0 {
621 break;
622 }
623 high = mid - 1;
624 }
625 }
626
627 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
628 let mut effective_wrap_idx = wrap_idx;
629 let can_hyphenate = hyphens == Hyphens::Auto
630 && wrap_idx == best
631 && best < boundaries.len() - 1
632 && is_break_inside_word(line_text, &boundaries, wrap_idx);
633 if can_hyphenate {
634 effective_wrap_idx = resolve_auto_hyphen_break(
635 measurer,
636 line_text,
637 style,
638 &boundaries,
639 start_idx,
640 wrap_idx,
641 );
642 }
643
644 let segment_start = boundaries[start_idx];
645 let mut segment_end = boundaries[effective_wrap_idx];
646 if wrap_idx != best {
647 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
648 }
649 wrapped.push(line.subsequence(segment_start..segment_end));
650
651 start_idx = if wrap_idx != best {
652 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
653 } else {
654 effective_wrap_idx
655 };
656 }
657
658 if wrapped.is_empty() {
659 wrapped.push(crate::text::AnnotatedString::from(""));
660 }
661
662 wrapped
663}
664
665fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
666 measurer: &M,
667 line: &crate::text::AnnotatedString,
668 style: &TextStyle,
669 max_width: f32,
670 line_break: LineBreak,
671) -> Option<Vec<crate::text::AnnotatedString>> {
672 let line_text = line.text.as_str();
673 let boundaries = char_boundaries(line_text);
674 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
675 if breakpoints.len() <= 2 {
676 return None;
677 }
678
679 let node_count = breakpoints.len();
680 let mut best_cost = vec![f32::INFINITY; node_count];
681 let mut next_index = vec![None; node_count];
682 best_cost[node_count - 1] = 0.0;
683
684 for start in (0..node_count - 1).rev() {
685 for end in start + 1..node_count {
686 let start_byte = boundaries[breakpoints[start]];
687 let end_byte = boundaries[breakpoints[end]];
688 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
689 if trimmed_end <= start_byte {
690 continue;
691 }
692 let segment = line.subsequence(start_byte..trimmed_end);
693 let segment_width = measurer.measure(&segment, style).width;
694 if segment_width > max_width + WRAP_EPSILON {
695 continue;
696 }
697 if !best_cost[end].is_finite() {
698 continue;
699 }
700 let slack = (max_width - segment_width).max(0.0);
701 let is_last = end == node_count - 1;
702 let segment_cost = match line_break {
703 LineBreak::Heading => slack * slack,
704 LineBreak::Paragraph => {
705 if is_last {
706 slack * slack * 0.16
707 } else {
708 slack * slack
709 }
710 }
711 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
712 };
713 let candidate = segment_cost + best_cost[end];
714 if candidate < best_cost[start] {
715 best_cost[start] = candidate;
716 next_index[start] = Some(end);
717 }
718 }
719 }
720
721 let mut wrapped = Vec::new();
722 let mut current = 0usize;
723 while current < node_count - 1 {
724 let next = next_index[current]?;
725 let start_byte = boundaries[breakpoints[current]];
726 let end_byte = boundaries[breakpoints[next]];
727 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
728 if trimmed_end <= start_byte {
729 return None;
730 }
731 wrapped.push(line.subsequence(start_byte..trimmed_end));
732 current = next;
733 }
734
735 if wrapped.is_empty() {
736 return None;
737 }
738
739 Some(wrapped)
740}
741
742fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
743 let mut points = vec![0usize];
744 for idx in 1..boundaries.len() - 1 {
745 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
746 let current = &line[boundaries[idx]..boundaries[idx + 1]];
747 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
748 points.push(idx);
749 }
750 }
751 let end = boundaries.len() - 1;
752 if points.last().copied() != Some(end) {
753 points.push(end);
754 }
755 points
756}
757
758fn choose_wrap_break(
759 line: &str,
760 boundaries: &[usize],
761 start_idx: usize,
762 best: usize,
763 _line_break: LineBreak,
764) -> usize {
765 if best >= boundaries.len() - 1 {
766 return best;
767 }
768
769 if best <= start_idx + 1 {
770 return best;
771 }
772
773 for idx in (start_idx + 1..best).rev() {
774 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
775 if prev.chars().all(char::is_whitespace) {
776 return idx;
777 }
778 }
779 best
780}
781
782fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
783 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
784 return false;
785 }
786 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
787 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
788 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
789}
790
791fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
792 measurer: &M,
793 line: &str,
794 style: &TextStyle,
795 boundaries: &[usize],
796 start_idx: usize,
797 break_idx: usize,
798) -> usize {
799 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx) {
800 if is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate) {
801 return candidate;
802 }
803 }
804 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
805}
806
807fn is_valid_auto_hyphen_break(
808 line: &str,
809 boundaries: &[usize],
810 start_idx: usize,
811 break_idx: usize,
812 candidate_idx: usize,
813) -> bool {
814 let end_idx = boundaries.len().saturating_sub(1);
815 candidate_idx > start_idx
816 && candidate_idx < end_idx
817 && candidate_idx <= break_idx
818 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
819 && is_break_inside_word(line, boundaries, candidate_idx)
820}
821
822fn choose_auto_hyphen_break_fallback(
823 boundaries: &[usize],
824 start_idx: usize,
825 break_idx: usize,
826) -> usize {
827 let end_idx = boundaries.len().saturating_sub(1);
828 if break_idx >= end_idx {
829 return break_idx;
830 }
831 let trailing_len = end_idx.saturating_sub(break_idx);
832 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
833 return break_idx;
834 }
835
836 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
837 let max_break = break_idx.saturating_sub(1);
838 if min_break > max_break {
839 return break_idx;
840 }
841
842 let mut best_break = break_idx;
843 let mut best_penalty = usize::MAX;
844 for idx in min_break..=max_break {
845 let candidate_trailing_len = end_idx.saturating_sub(idx);
846 let candidate_prefix_len = idx.saturating_sub(start_idx);
847 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
848 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
849 {
850 continue;
851 }
852
853 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
854 if penalty < best_penalty {
855 best_penalty = penalty;
856 best_break = idx;
857 if penalty == 0 {
858 break;
859 }
860 }
861 }
862 best_break
863}
864
865fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
866 while idx < boundaries.len() - 1 {
867 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
868 if !ch.chars().all(char::is_whitespace) {
869 break;
870 }
871 idx += 1;
872 }
873 idx
874}
875
876fn apply_line_overflow<M: TextMeasurer + ?Sized>(
877 measurer: &M,
878 line: &str,
879 style: &TextStyle,
880 max_width: Option<f32>,
881 options: TextLayoutOptions,
882 is_last_visible_line: bool,
883 single_line_ellipsis: bool,
884) -> String {
885 if options.overflow == TextOverflow::Clip || !is_last_visible_line {
886 return line.to_string();
887 }
888
889 let Some(width_limit) = max_width else {
890 return match options.overflow {
891 TextOverflow::Ellipsis => format!("{line}{ELLIPSIS}"),
892 TextOverflow::StartEllipsis => format!("{ELLIPSIS}{line}"),
893 TextOverflow::MiddleEllipsis => format!("{ELLIPSIS}{line}"),
894 TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
895 };
896 };
897
898 match options.overflow {
899 TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
900 TextOverflow::Ellipsis => fit_end_ellipsis(measurer, line, style, width_limit),
901 TextOverflow::StartEllipsis => {
902 if single_line_ellipsis {
903 fit_start_ellipsis(measurer, line, style, width_limit)
904 } else {
905 line.to_string()
906 }
907 }
908 TextOverflow::MiddleEllipsis => {
909 if single_line_ellipsis {
910 fit_middle_ellipsis(measurer, line, style, width_limit)
911 } else {
912 line.to_string()
913 }
914 }
915 }
916}
917
918fn fit_end_ellipsis<M: TextMeasurer + ?Sized>(
919 measurer: &M,
920 line: &str,
921 style: &TextStyle,
922 max_width: f32,
923) -> String {
924 if measurer
925 .measure(&crate::text::AnnotatedString::from(line), style)
926 .width
927 <= max_width + WRAP_EPSILON
928 {
929 return line.to_string();
930 }
931
932 let ellipsis_width = measurer
933 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
934 .width;
935 if ellipsis_width > max_width + WRAP_EPSILON {
936 return String::new();
937 }
938
939 let boundaries = char_boundaries(line);
940 let mut low = 0usize;
941 let mut high = boundaries.len() - 1;
942 let mut best = 0usize;
943
944 while low <= high {
945 let mid = (low + high) / 2;
946 let prefix = &line[..boundaries[mid]];
947 let candidate = format!("{prefix}{ELLIPSIS}");
948 let width = measurer
949 .measure(
950 &crate::text::AnnotatedString::from(candidate.as_str()),
951 style,
952 )
953 .width;
954 if width <= max_width + WRAP_EPSILON {
955 best = mid;
956 low = mid + 1;
957 } else if mid == 0 {
958 break;
959 } else {
960 high = mid - 1;
961 }
962 }
963
964 format!("{}{}", &line[..boundaries[best]], ELLIPSIS)
965}
966
967fn fit_start_ellipsis<M: TextMeasurer + ?Sized>(
968 measurer: &M,
969 line: &str,
970 style: &TextStyle,
971 max_width: f32,
972) -> String {
973 if measurer
974 .measure(&crate::text::AnnotatedString::from(line), style)
975 .width
976 <= max_width + WRAP_EPSILON
977 {
978 return line.to_string();
979 }
980
981 let ellipsis_width = measurer
982 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
983 .width;
984 if ellipsis_width > max_width + WRAP_EPSILON {
985 return String::new();
986 }
987
988 let boundaries = char_boundaries(line);
989 let mut low = 0usize;
990 let mut high = boundaries.len() - 1;
991 let mut best = boundaries.len() - 1;
992
993 while low <= high {
994 let mid = (low + high) / 2;
995 let suffix = &line[boundaries[mid]..];
996 let candidate = format!("{ELLIPSIS}{suffix}");
997 let width = measurer
998 .measure(
999 &crate::text::AnnotatedString::from(candidate.as_str()),
1000 style,
1001 )
1002 .width;
1003 if width <= max_width + WRAP_EPSILON {
1004 best = mid;
1005 if mid == 0 {
1006 break;
1007 }
1008 high = mid - 1;
1009 } else {
1010 low = mid + 1;
1011 }
1012 }
1013
1014 format!("{ELLIPSIS}{}", &line[boundaries[best]..])
1015}
1016
1017fn fit_middle_ellipsis<M: TextMeasurer + ?Sized>(
1018 measurer: &M,
1019 line: &str,
1020 style: &TextStyle,
1021 max_width: f32,
1022) -> String {
1023 if measurer
1024 .measure(&crate::text::AnnotatedString::from(line), style)
1025 .width
1026 <= max_width + WRAP_EPSILON
1027 {
1028 return line.to_string();
1029 }
1030
1031 let ellipsis_width = measurer
1032 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1033 .width;
1034 if ellipsis_width > max_width + WRAP_EPSILON {
1035 return String::new();
1036 }
1037
1038 let boundaries = char_boundaries(line);
1039 let total_chars = boundaries.len().saturating_sub(1);
1040 for keep in (0..=total_chars).rev() {
1041 let keep_start = keep.div_ceil(2);
1042 let keep_end = keep / 2;
1043 let start = &line[..boundaries[keep_start]];
1044 let end_start = boundaries[total_chars.saturating_sub(keep_end)];
1045 let end = &line[end_start..];
1046 let candidate = format!("{start}{ELLIPSIS}{end}");
1047 if measurer
1048 .measure(
1049 &crate::text::AnnotatedString::from(candidate.as_str()),
1050 style,
1051 )
1052 .width
1053 <= max_width + WRAP_EPSILON
1054 {
1055 return candidate;
1056 }
1057 }
1058
1059 ELLIPSIS.to_string()
1060}
1061
1062fn char_boundaries(text: &str) -> Vec<usize> {
1063 let mut out = Vec::with_capacity(text.chars().count() + 1);
1064 out.push(0);
1065 for (idx, _) in text.char_indices() {
1066 if idx != 0 {
1067 out.push(idx);
1068 }
1069 }
1070 out.push(text.len());
1071 out
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076 use super::*;
1077 use crate::text::{Hyphens, LineBreak, ParagraphStyle, TextUnit};
1078 use crate::text_layout_result::TextLayoutResult;
1079
1080 struct ContractBreakMeasurer {
1081 retreat: usize,
1082 }
1083
1084 impl TextMeasurer for ContractBreakMeasurer {
1085 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
1086 MonospacedTextMeasurer.measure(
1087 &crate::text::AnnotatedString::from(text.text.as_str()),
1088 style,
1089 )
1090 }
1091
1092 fn get_offset_for_position(
1093 &self,
1094 text: &crate::text::AnnotatedString,
1095 style: &TextStyle,
1096 x: f32,
1097 y: f32,
1098 ) -> usize {
1099 MonospacedTextMeasurer.get_offset_for_position(
1100 &crate::text::AnnotatedString::from(text.text.as_str()),
1101 style,
1102 x,
1103 y,
1104 )
1105 }
1106
1107 fn get_cursor_x_for_offset(
1108 &self,
1109 text: &crate::text::AnnotatedString,
1110 style: &TextStyle,
1111 offset: usize,
1112 ) -> f32 {
1113 MonospacedTextMeasurer.get_cursor_x_for_offset(
1114 &crate::text::AnnotatedString::from(text.text.as_str()),
1115 style,
1116 offset,
1117 )
1118 }
1119
1120 fn layout(
1121 &self,
1122 text: &crate::text::AnnotatedString,
1123 style: &TextStyle,
1124 ) -> TextLayoutResult {
1125 MonospacedTextMeasurer.layout(
1126 &crate::text::AnnotatedString::from(text.text.as_str()),
1127 style,
1128 )
1129 }
1130
1131 fn choose_auto_hyphen_break(
1132 &self,
1133 _line: &str,
1134 _style: &TextStyle,
1135 _segment_start_char: usize,
1136 measured_break_char: usize,
1137 ) -> Option<usize> {
1138 measured_break_char.checked_sub(self.retreat)
1139 }
1140 }
1141
1142 fn style_with_line_break(line_break: LineBreak) -> TextStyle {
1143 TextStyle {
1144 span_style: crate::text::SpanStyle {
1145 font_size: TextUnit::Sp(10.0),
1146 ..Default::default()
1147 },
1148 paragraph_style: ParagraphStyle {
1149 line_break,
1150 ..Default::default()
1151 },
1152 }
1153 }
1154
1155 fn style_with_hyphens(hyphens: Hyphens) -> TextStyle {
1156 TextStyle {
1157 span_style: crate::text::SpanStyle {
1158 font_size: TextUnit::Sp(10.0),
1159 ..Default::default()
1160 },
1161 paragraph_style: ParagraphStyle {
1162 hyphens,
1163 ..Default::default()
1164 },
1165 }
1166 }
1167
1168 #[test]
1169 fn text_layout_options_wraps_and_limits_lines() {
1170 let style = TextStyle {
1171 span_style: crate::text::SpanStyle {
1172 font_size: TextUnit::Sp(10.0),
1173 ..Default::default()
1174 },
1175 ..Default::default()
1176 };
1177 let options = TextLayoutOptions {
1178 overflow: TextOverflow::Clip,
1179 soft_wrap: true,
1180 max_lines: 2,
1181 min_lines: 1,
1182 };
1183
1184 let prepared = prepare_text_layout(
1185 &crate::text::AnnotatedString::from("A B C D E F"),
1186 &style,
1187 options,
1188 Some(24.0), );
1190
1191 assert!(prepared.did_overflow);
1192 assert!(prepared.metrics.line_count <= 2);
1193 }
1194
1195 #[test]
1196 fn text_layout_options_end_ellipsis_applies() {
1197 let style = TextStyle {
1198 span_style: crate::text::SpanStyle {
1199 font_size: TextUnit::Sp(10.0),
1200 ..Default::default()
1201 },
1202 ..Default::default()
1203 };
1204 let options = TextLayoutOptions {
1205 overflow: TextOverflow::Ellipsis,
1206 soft_wrap: false,
1207 max_lines: 1,
1208 min_lines: 1,
1209 };
1210
1211 let prepared = prepare_text_layout(
1212 &crate::text::AnnotatedString::from("Long long line"),
1213 &style,
1214 options,
1215 Some(20.0),
1216 );
1217 assert!(prepared.did_overflow);
1218 assert!(prepared.text.text.contains(ELLIPSIS));
1219 }
1220
1221 #[test]
1222 fn text_layout_options_visible_keeps_full_text() {
1223 let style = TextStyle {
1224 span_style: crate::text::SpanStyle {
1225 font_size: TextUnit::Sp(10.0),
1226 ..Default::default()
1227 },
1228 ..Default::default()
1229 };
1230 let options = TextLayoutOptions {
1231 overflow: TextOverflow::Visible,
1232 soft_wrap: false,
1233 max_lines: 1,
1234 min_lines: 1,
1235 };
1236
1237 let input = "This should remain unchanged";
1238 let prepared = prepare_text_layout(
1239 &crate::text::AnnotatedString::from(input),
1240 &style,
1241 options,
1242 Some(10.0),
1243 );
1244 assert_eq!(prepared.text.text, input);
1245 }
1246
1247 #[test]
1248 fn text_layout_options_respects_min_lines() {
1249 let style = TextStyle {
1250 span_style: crate::text::SpanStyle {
1251 font_size: TextUnit::Sp(10.0),
1252 ..Default::default()
1253 },
1254 ..Default::default()
1255 };
1256 let options = TextLayoutOptions {
1257 overflow: TextOverflow::Clip,
1258 soft_wrap: true,
1259 max_lines: 4,
1260 min_lines: 3,
1261 };
1262
1263 let prepared = prepare_text_layout(
1264 &crate::text::AnnotatedString::from("short"),
1265 &style,
1266 options,
1267 Some(100.0),
1268 );
1269 assert_eq!(prepared.metrics.line_count, 3);
1270 }
1271
1272 #[test]
1273 fn text_layout_options_middle_ellipsis_for_single_line() {
1274 let style = TextStyle {
1275 span_style: crate::text::SpanStyle {
1276 font_size: TextUnit::Sp(10.0),
1277 ..Default::default()
1278 },
1279 ..Default::default()
1280 };
1281 let options = TextLayoutOptions {
1282 overflow: TextOverflow::MiddleEllipsis,
1283 soft_wrap: false,
1284 max_lines: 1,
1285 min_lines: 1,
1286 };
1287
1288 let prepared = prepare_text_layout(
1289 &crate::text::AnnotatedString::from("abcdefghijk"),
1290 &style,
1291 options,
1292 Some(24.0),
1293 );
1294 assert!(prepared.text.text.contains(ELLIPSIS));
1295 assert!(prepared.did_overflow);
1296 }
1297
1298 #[test]
1299 fn text_layout_options_does_not_wrap_on_tiny_width_delta() {
1300 let style = TextStyle {
1301 span_style: crate::text::SpanStyle {
1302 font_size: TextUnit::Sp(10.0),
1303 ..Default::default()
1304 },
1305 ..Default::default()
1306 };
1307 let options = TextLayoutOptions {
1308 overflow: TextOverflow::Clip,
1309 soft_wrap: true,
1310 max_lines: usize::MAX,
1311 min_lines: 1,
1312 };
1313
1314 let text = "if counter % 2 == 0";
1315 let exact_width = measure_text(&crate::text::AnnotatedString::from(text), &style).width;
1316 let prepared = prepare_text_layout(
1317 &crate::text::AnnotatedString::from(text),
1318 &style,
1319 options,
1320 Some(exact_width - 0.1),
1321 );
1322
1323 assert!(
1324 !prepared.text.text.contains('\n'),
1325 "unexpected line split: {:?}",
1326 prepared.text
1327 );
1328 }
1329
1330 #[test]
1331 fn line_break_mode_changes_wrap_strategy_contract() {
1332 let text = "This is an example text";
1333 let options = TextLayoutOptions {
1334 overflow: TextOverflow::Clip,
1335 soft_wrap: true,
1336 max_lines: usize::MAX,
1337 min_lines: 1,
1338 };
1339
1340 let simple = prepare_text_layout(
1341 &crate::text::AnnotatedString::from(text),
1342 &style_with_line_break(LineBreak::Simple),
1343 options,
1344 Some(120.0),
1345 );
1346 let heading = prepare_text_layout(
1347 &crate::text::AnnotatedString::from(text),
1348 &style_with_line_break(LineBreak::Heading),
1349 options,
1350 Some(120.0),
1351 );
1352 let paragraph = prepare_text_layout(
1353 &crate::text::AnnotatedString::from(text),
1354 &style_with_line_break(LineBreak::Paragraph),
1355 options,
1356 Some(50.0),
1357 );
1358
1359 assert_eq!(
1360 simple.text.text.lines().collect::<Vec<_>>(),
1361 vec!["This is an example", "text"]
1362 );
1363 assert_eq!(
1364 heading.text.text.lines().collect::<Vec<_>>(),
1365 vec!["This is an", "example text"]
1366 );
1367 assert_eq!(
1368 paragraph.text.text.lines().collect::<Vec<_>>(),
1369 vec!["This", "is an", "example", "text"]
1370 );
1371 }
1372
1373 #[test]
1374 fn hyphens_mode_changes_wrap_strategy_contract() {
1375 let text = "Transformation";
1376 let options = TextLayoutOptions {
1377 overflow: TextOverflow::Clip,
1378 soft_wrap: true,
1379 max_lines: usize::MAX,
1380 min_lines: 1,
1381 };
1382
1383 let auto = prepare_text_layout(
1384 &crate::text::AnnotatedString::from(text),
1385 &style_with_hyphens(Hyphens::Auto),
1386 options,
1387 Some(24.0),
1388 );
1389 let none = prepare_text_layout(
1390 &crate::text::AnnotatedString::from(text),
1391 &style_with_hyphens(Hyphens::None),
1392 options,
1393 Some(24.0),
1394 );
1395
1396 assert_eq!(
1397 auto.text.text.lines().collect::<Vec<_>>(),
1398 vec!["Tran", "sfor", "ma", "tion"]
1399 );
1400 assert_eq!(
1401 none.text.text.lines().collect::<Vec<_>>(),
1402 vec!["Tran", "sfor", "mati", "on"]
1403 );
1404 assert!(
1405 !auto.text.text.contains('-'),
1406 "automatic hyphenation should influence breaks without mutating source text content"
1407 );
1408 }
1409
1410 #[test]
1411 fn hyphens_auto_uses_measurer_hyphen_contract_when_valid() {
1412 let text = "Transformation";
1413 let style = style_with_hyphens(Hyphens::Auto);
1414 let options = TextLayoutOptions {
1415 overflow: TextOverflow::Clip,
1416 soft_wrap: true,
1417 max_lines: usize::MAX,
1418 min_lines: 1,
1419 };
1420
1421 let prepared = prepare_text_layout_fallback(
1422 &ContractBreakMeasurer { retreat: 1 },
1423 &crate::text::AnnotatedString::from(text),
1424 &style,
1425 options,
1426 Some(24.0),
1427 );
1428
1429 assert_eq!(
1430 prepared.text.text.lines().collect::<Vec<_>>(),
1431 vec!["Tra", "nsf", "orm", "ati", "on"]
1432 );
1433 }
1434
1435 #[test]
1436 fn hyphens_auto_falls_back_when_measurer_hyphen_contract_is_invalid() {
1437 let text = "Transformation";
1438 let style = style_with_hyphens(Hyphens::Auto);
1439 let options = TextLayoutOptions {
1440 overflow: TextOverflow::Clip,
1441 soft_wrap: true,
1442 max_lines: usize::MAX,
1443 min_lines: 1,
1444 };
1445
1446 let prepared = prepare_text_layout_fallback(
1447 &ContractBreakMeasurer { retreat: 10 },
1448 &crate::text::AnnotatedString::from(text),
1449 &style,
1450 options,
1451 Some(24.0),
1452 );
1453
1454 assert_eq!(
1455 prepared.text.text.lines().collect::<Vec<_>>(),
1456 vec!["Tran", "sfor", "ma", "tion"]
1457 );
1458 }
1459
1460 #[test]
1461 fn transformed_text_keeps_span_ranges_within_display_bounds() {
1462 let style = TextStyle {
1463 span_style: crate::text::SpanStyle {
1464 font_size: TextUnit::Sp(10.0),
1465 ..Default::default()
1466 },
1467 ..Default::default()
1468 };
1469 let options = TextLayoutOptions {
1470 overflow: TextOverflow::Ellipsis,
1471 soft_wrap: false,
1472 max_lines: 1,
1473 min_lines: 1,
1474 };
1475 let annotated = crate::text::AnnotatedString::builder()
1476 .push_style(crate::text::SpanStyle {
1477 font_weight: Some(crate::text::FontWeight::BOLD),
1478 ..Default::default()
1479 })
1480 .append("Styled overflow text sample")
1481 .pop()
1482 .to_annotated_string();
1483
1484 let prepared = prepare_text_layout(&annotated, &style, options, Some(40.0));
1485 assert!(prepared.did_overflow);
1486 for span in &prepared.text.span_styles {
1487 assert!(span.range.start < span.range.end);
1488 assert!(span.range.end <= prepared.text.text.len());
1489 assert!(prepared.text.text.is_char_boundary(span.range.start));
1490 assert!(prepared.text.text.is_char_boundary(span.range.end));
1491 }
1492 }
1493
1494 #[test]
1495 fn wrapped_text_splits_styles_around_inserted_newlines() {
1496 let style = TextStyle {
1497 span_style: crate::text::SpanStyle {
1498 font_size: TextUnit::Sp(10.0),
1499 ..Default::default()
1500 },
1501 ..Default::default()
1502 };
1503 let options = TextLayoutOptions {
1504 overflow: TextOverflow::Clip,
1505 soft_wrap: true,
1506 max_lines: usize::MAX,
1507 min_lines: 1,
1508 };
1509 let annotated = crate::text::AnnotatedString::builder()
1510 .push_style(crate::text::SpanStyle {
1511 text_decoration: Some(crate::text::TextDecoration::UNDERLINE),
1512 ..Default::default()
1513 })
1514 .append("Wrapped style text example")
1515 .pop()
1516 .to_annotated_string();
1517
1518 let prepared = prepare_text_layout(&annotated, &style, options, Some(32.0));
1519 assert!(prepared.text.text.contains('\n'));
1520 assert!(!prepared.text.span_styles.is_empty());
1521 for span in &prepared.text.span_styles {
1522 assert!(span.range.end <= prepared.text.text.len());
1523 }
1524 }
1525
1526 #[test]
1527 fn mixed_font_size_segments_wrap_without_truncation() {
1528 let style = TextStyle {
1529 span_style: crate::text::SpanStyle {
1530 font_size: TextUnit::Sp(14.0),
1531 ..Default::default()
1532 },
1533 ..Default::default()
1534 };
1535 let options = TextLayoutOptions {
1536 overflow: TextOverflow::Clip,
1537 soft_wrap: true,
1538 max_lines: usize::MAX,
1539 min_lines: 1,
1540 };
1541 let annotated = crate::text::AnnotatedString::builder()
1542 .append("You can also ")
1543 .push_style(crate::text::SpanStyle {
1544 font_size: TextUnit::Sp(22.0),
1545 ..Default::default()
1546 })
1547 .append("change font size")
1548 .pop()
1549 .append(" dynamically mid-sentence!")
1550 .to_annotated_string();
1551
1552 let prepared = prepare_text_layout(&annotated, &style, options, Some(260.0));
1553 assert!(prepared.text.text.contains('\n'));
1554 assert!(prepared.text.text.contains("mid-sentence!"));
1555 assert!(!prepared.did_overflow);
1556 }
1557}