1use std::{
2 borrow::Cow,
3 cell::{Cell, RefCell},
4 collections::{HashMap, VecDeque, hash_map::Entry},
5 hash::Hash,
6 ops::Range,
7 rc::Rc,
8};
9
10use cranpose_core::NodeId;
11use web_time::Instant;
12
13use super::{
14 layout_options::{TextLayoutOptions, TextOverflow},
15 paragraph::{Hyphens, LineBreak},
16 style::TextStyle,
17};
18use crate::{font_scale::FontScaleCurve, text_layout_result::TextLayoutResult};
19
20const ELLIPSIS: &str = "\u{2026}";
21const DEFAULT_FONT_SIZE_SP: f32 = 14.0;
22const WRAP_EPSILON: f32 = 0.5;
23const SCALE_DOWN_SEARCH_STEPS: usize = 14;
24const AUTO_HYPHEN_MIN_SEGMENT_CHARS: usize = 2;
25const AUTO_HYPHEN_MIN_TRAILING_CHARS: usize = 3;
26const AUTO_HYPHEN_PREFERRED_TRAILING_CHARS: usize = 4;
27const TEXT_SERVICE_CACHE_CAPACITY: usize = 8192;
28const TEXT_LAYOUT_TELEMETRY_ENV: &str = "CRANPOSE_TEXT_LAYOUT_TELEMETRY";
29
30fn text_layout_telemetry_enabled() -> bool {
31 cranpose_core::env_flag!(TEXT_LAYOUT_TELEMETRY_ENV)
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct TextMetrics {
36 pub width: f32,
37 pub height: f32,
38 pub line_height: f32,
40 pub line_count: usize,
42}
43
44#[derive(Clone, Debug, PartialEq)]
45pub struct PreparedTextLayout {
46 pub text: crate::text::AnnotatedString,
47 pub visual_style: TextStyle,
48 pub metrics: TextMetrics,
49 pub did_overflow: bool,
50}
51
52#[derive(Clone, Debug, PartialEq)]
53pub struct TextLinePrefixWidths {
54 prefix_widths: Vec<f32>,
55 separator_before: Vec<f32>,
56 non_empty_overhang: f32,
57}
58
59impl TextLinePrefixWidths {
60 pub fn from_parts(
61 prefix_widths: Vec<f32>,
62 separator_before: Vec<f32>,
63 non_empty_overhang: f32,
64 ) -> Option<Self> {
65 if prefix_widths.is_empty() || prefix_widths.len() != separator_before.len() + 1 {
66 return None;
67 }
68 if prefix_widths
69 .iter()
70 .chain(separator_before.iter())
71 .any(|value| !value.is_finite())
72 {
73 return None;
74 }
75 let non_empty_overhang = non_empty_overhang.max(0.0);
76 if !non_empty_overhang.is_finite() {
77 return None;
78 }
79 Some(Self {
80 prefix_widths,
81 separator_before,
82 non_empty_overhang,
83 })
84 }
85
86 pub fn monospaced(char_count: usize, char_width: f32, letter_spacing: f32) -> Option<Self> {
87 if !char_width.is_finite() || !letter_spacing.is_finite() {
88 return None;
89 }
90 let char_width = char_width.max(0.0);
91 let letter_spacing = letter_spacing.max(0.0);
92 let mut prefix_widths = Vec::with_capacity(char_count + 1);
93 let mut separator_before = Vec::with_capacity(char_count);
94 let mut width = 0.0f32;
95 prefix_widths.push(width);
96 for _ in 0..char_count {
97 separator_before.push(0.0);
98 width += char_width + letter_spacing;
99 prefix_widths.push(width);
100 }
101 Self::from_parts(prefix_widths, separator_before, 0.0)
102 }
103
104 pub fn char_count(&self) -> usize {
105 self.separator_before.len()
106 }
107
108 pub fn width_for_char_range(&self, start: usize, end: usize) -> Option<f32> {
109 if start > end || end > self.char_count() {
110 return None;
111 }
112 if start == end {
113 return Some(0.0);
114 }
115 let separator = self.separator_before.get(start).copied().unwrap_or(0.0);
116 Some(
117 (self.prefix_widths[end] - self.prefix_widths[start] - separator).max(0.0)
118 + self.non_empty_overhang,
119 )
120 }
121}
122
123pub trait TextMeasurer: 'static {
124 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
125
126 fn measure_for_node(
127 &self,
128 node_id: Option<NodeId>,
129 text: &crate::text::AnnotatedString,
130 style: &TextStyle,
131 ) -> TextMetrics {
132 let _ = node_id;
133 self.measure(text, style)
134 }
135
136 fn measure_subsequence(
137 &self,
138 text: &crate::text::AnnotatedString,
139 range: Range<usize>,
140 style: &TextStyle,
141 ) -> TextMetrics {
142 self.measure(&text.subsequence(range), style)
143 }
144
145 fn measure_subsequence_for_node(
146 &self,
147 node_id: Option<NodeId>,
148 text: &crate::text::AnnotatedString,
149 range: Range<usize>,
150 style: &TextStyle,
151 ) -> TextMetrics {
152 let _ = node_id;
153 self.measure_subsequence(text, range, style)
154 }
155
156 fn measure_line_prefix_widths(
157 &self,
158 text: &crate::text::AnnotatedString,
159 line_range: Range<usize>,
160 style: &TextStyle,
161 ) -> Option<TextLinePrefixWidths> {
162 let _ = text;
163 let _ = line_range;
164 let _ = style;
165 None
166 }
167
168 fn measure_line_width(
169 &self,
170 text: &crate::text::AnnotatedString,
171 line_range: Range<usize>,
172 style: &TextStyle,
173 ) -> Option<f32> {
174 let _ = text;
175 let _ = line_range;
176 let _ = style;
177 None
178 }
179
180 fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
181 self.measure(text, style).line_height
182 }
183
184 fn glyph_line_box(&self, style: &TextStyle) -> Option<(f32, f32)> {
193 let _ = style;
194 None
195 }
196
197 fn first_baseline(&self, style: &TextStyle) -> Option<f32> {
203 let _ = style;
204 None
205 }
206
207 fn line_box(&self, style: &TextStyle) -> Option<crate::text::LineBox> {
215 let baseline = self.first_baseline(style)?;
216 Some(crate::text::LineBox {
217 height: self.line_height(&crate::text::AnnotatedString::default(), style),
218 baseline,
219 })
220 }
221
222 fn line_height_for_node(
223 &self,
224 node_id: Option<NodeId>,
225 text: &crate::text::AnnotatedString,
226 style: &TextStyle,
227 ) -> f32 {
228 let _ = node_id;
229 self.line_height(text, style)
230 }
231
232 fn get_offset_for_position(
233 &self,
234 text: &crate::text::AnnotatedString,
235 style: &TextStyle,
236 x: f32,
237 y: f32,
238 ) -> usize;
239
240 fn get_cursor_x_for_offset(
241 &self,
242 text: &crate::text::AnnotatedString,
243 style: &TextStyle,
244 offset: usize,
245 ) -> f32;
246
247 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
248
249 fn choose_auto_hyphen_break(
255 &self,
256 _line: &str,
257 _style: &TextStyle,
258 _segment_start_char: usize,
259 _measured_break_char: usize,
260 ) -> Option<usize> {
261 None
262 }
263
264 fn measure_with_options(
265 &self,
266 text: &crate::text::AnnotatedString,
267 style: &TextStyle,
268 options: TextLayoutOptions,
269 max_width: Option<f32>,
270 ) -> TextMetrics {
271 self.prepare_with_options(text, style, options, max_width)
272 .metrics
273 }
274
275 fn measure_with_options_for_node(
276 &self,
277 node_id: Option<NodeId>,
278 text: &crate::text::AnnotatedString,
279 style: &TextStyle,
280 options: TextLayoutOptions,
281 max_width: Option<f32>,
282 ) -> TextMetrics {
283 self.prepare_with_options_for_node(node_id, text, style, options, max_width)
284 .metrics
285 }
286
287 fn prepare_with_options(
288 &self,
289 text: &crate::text::AnnotatedString,
290 style: &TextStyle,
291 options: TextLayoutOptions,
292 max_width: Option<f32>,
293 ) -> PreparedTextLayout {
294 self.prepare_with_options_fallback(text, style, options, max_width)
295 }
296
297 fn prepare_with_options_for_node(
298 &self,
299 node_id: Option<NodeId>,
300 text: &crate::text::AnnotatedString,
301 style: &TextStyle,
302 options: TextLayoutOptions,
303 max_width: Option<f32>,
304 ) -> PreparedTextLayout {
305 prepare_text_layout_with_measurer_for_node(self, node_id, text, style, options, max_width)
306 }
307
308 fn prepare_with_options_fallback(
309 &self,
310 text: &crate::text::AnnotatedString,
311 style: &TextStyle,
312 options: TextLayoutOptions,
313 max_width: Option<f32>,
314 ) -> PreparedTextLayout {
315 prepare_text_layout_fallback(self, text, style, options, max_width)
316 }
317}
318
319#[derive(Default)]
320struct MonospacedTextMeasurer;
321
322impl MonospacedTextMeasurer {
323 const DEFAULT_SIZE: f32 = 14.0;
324 const CHAR_WIDTH_RATIO: f32 = 0.6;
325
326 fn get_metrics(style: &TextStyle) -> (f32, f32) {
327 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
328 let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
329 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
330 (
331 (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
332 line_height,
333 )
334 }
335}
336
337impl TextMeasurer for MonospacedTextMeasurer {
338 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
339 let (char_width, line_height) = Self::get_metrics(style);
340
341 let lines: Vec<&str> = text.text.split('\n').collect();
342 let line_count = lines.len().max(1);
343
344 let width = lines
345 .iter()
346 .map(|line| line.chars().count() as f32 * char_width)
347 .fold(0.0_f32, f32::max);
348
349 TextMetrics {
350 width,
351 height: line_count as f32 * line_height,
352 line_height,
353 line_count,
354 }
355 }
356
357 fn measure_subsequence(
358 &self,
359 text: &crate::text::AnnotatedString,
360 range: Range<usize>,
361 style: &TextStyle,
362 ) -> TextMetrics {
363 let (char_width, line_height) = Self::get_metrics(style);
364 let slice = &text.text[range];
365 let line_count = slice.split('\n').count().max(1);
366 let width = slice
367 .split('\n')
368 .map(|line| line.chars().count() as f32 * char_width)
369 .fold(0.0_f32, f32::max);
370
371 TextMetrics {
372 width,
373 height: line_count as f32 * line_height,
374 line_height,
375 line_count,
376 }
377 }
378
379 fn measure_line_prefix_widths(
380 &self,
381 text: &crate::text::AnnotatedString,
382 line_range: Range<usize>,
383 style: &TextStyle,
384 ) -> Option<TextLinePrefixWidths> {
385 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
386 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE);
387 TextLinePrefixWidths::monospaced(
388 text.text[line_range].chars().count(),
389 font_size * Self::CHAR_WIDTH_RATIO,
390 letter_spacing,
391 )
392 }
393
394 fn measure_line_width(
395 &self,
396 text: &crate::text::AnnotatedString,
397 line_range: Range<usize>,
398 style: &TextStyle,
399 ) -> Option<f32> {
400 Some(self.measure_subsequence(text, line_range, style).width)
401 }
402
403 fn line_height(&self, _text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
404 let (_, line_height) = Self::get_metrics(style);
405 line_height
406 }
407
408 fn get_offset_for_position(
409 &self,
410 text: &crate::text::AnnotatedString,
411 style: &TextStyle,
412 x: f32,
413 y: f32,
414 ) -> usize {
415 let (char_width, line_height) = Self::get_metrics(style);
416
417 if text.text.is_empty() {
418 return 0;
419 }
420
421 let line_index = (y / line_height).floor().max(0.0) as usize;
422 let lines: Vec<&str> = text.text.split('\n').collect();
423 let target_line = line_index.min(lines.len().saturating_sub(1));
424
425 let mut line_start_byte = 0;
426 for line in lines.iter().take(target_line) {
427 line_start_byte += line.len() + 1;
428 }
429
430 let line_text = lines.get(target_line).unwrap_or(&"");
431 let char_index = (x / char_width).round() as usize;
432 let line_char_count = line_text.chars().count();
433 let clamped_index = char_index.min(line_char_count);
434
435 let offset_in_line = line_text
436 .char_indices()
437 .nth(clamped_index)
438 .map(|(i, _)| i)
439 .unwrap_or(line_text.len());
440
441 line_start_byte + offset_in_line
442 }
443
444 fn get_cursor_x_for_offset(
445 &self,
446 text: &crate::text::AnnotatedString,
447 style: &TextStyle,
448 offset: usize,
449 ) -> f32 {
450 let (char_width, _) = Self::get_metrics(style);
451
452 let clamped_offset = offset.min(text.text.len());
453 let char_count = text.text[..clamped_offset].chars().count();
454 char_count as f32 * char_width
455 }
456
457 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
458 let (char_width, line_height) = Self::get_metrics(style);
459 TextLayoutResult::monospaced(&text.text, char_width, line_height)
460 }
461}
462
463#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
464struct TextBaseCacheKey {
465 text_hash: u64,
466 style_hash: u64,
467}
468
469#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
470struct TextOptionsCacheKey {
471 base: TextBaseCacheKey,
472 options: TextLayoutOptions,
473 max_width_bits: Option<u32>,
474}
475
476#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
477struct TextPreparedCacheKey {
478 base: TextOptionsCacheKey,
479 visual_hash: u64,
480}
481
482struct BoundedTextCache<K, V> {
483 capacity: usize,
484 entries: HashMap<K, V>,
485 order: VecDeque<K>,
486}
487
488impl<K, V> BoundedTextCache<K, V>
489where
490 K: Clone + Eq + Hash,
491 V: Clone,
492{
493 fn new(capacity: usize) -> Self {
494 Self {
495 capacity,
496 entries: HashMap::new(),
497 order: VecDeque::new(),
498 }
499 }
500
501 fn clear(&mut self) {
502 self.entries.clear();
503 self.order.clear();
504 }
505
506 fn get(&self, key: &K) -> Option<V> {
507 self.entries.get(key).cloned()
508 }
509
510 fn insert(&mut self, key: K, value: V) {
511 match self.entries.entry(key.clone()) {
512 Entry::Occupied(mut entry) => {
513 entry.insert(value);
514 return;
515 }
516 Entry::Vacant(_) => {}
517 }
518 if self.entries.len() == self.capacity {
519 while let Some(evicted) = self.order.pop_front() {
520 if self.entries.remove(&evicted).is_some() {
521 break;
522 }
523 }
524 }
525 self.order.push_back(key.clone());
526 self.entries.insert(key, value);
527 }
528}
529
530pub(crate) struct TextService {
531 generation: Cell<u64>,
532 measurer: RefCell<Rc<dyn TextMeasurer>>,
533 metrics_cache: RefCell<BoundedTextCache<TextBaseCacheKey, TextMetrics>>,
534 options_metrics_cache: RefCell<BoundedTextCache<TextOptionsCacheKey, TextMetrics>>,
535 prepared_cache: RefCell<BoundedTextCache<TextPreparedCacheKey, PreparedTextLayout>>,
536 layout_cache: RefCell<BoundedTextCache<TextBaseCacheKey, TextLayoutResult>>,
537}
538
539impl TextService {
540 pub(crate) fn new() -> Self {
541 Self::from_measurer(Rc::new(MonospacedTextMeasurer))
542 }
543
544 pub(crate) fn from_measurer(measurer: Rc<dyn TextMeasurer>) -> Self {
545 Self {
546 generation: Cell::new(1),
547 measurer: RefCell::new(measurer),
548 metrics_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
549 options_metrics_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
550 prepared_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
551 layout_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
552 }
553 }
554
555 pub(crate) fn set_measurer(&self, measurer: Rc<dyn TextMeasurer>) {
556 *self.measurer.borrow_mut() = measurer;
557 self.clear_caches();
558 }
559
560 pub(crate) fn generation(&self) -> u64 {
561 self.generation.get()
562 }
563
564 pub(crate) fn current_measurer(&self) -> Rc<dyn TextMeasurer> {
565 Rc::clone(&self.measurer.borrow())
566 }
567
568 pub(crate) fn with_measurer<R>(&self, f: impl FnOnce(&dyn TextMeasurer) -> R) -> R {
569 let measurer = self.current_measurer();
570 f(&*measurer)
571 }
572
573 pub(crate) fn measure(
574 &self,
575 node_id: Option<NodeId>,
576 text: &crate::text::AnnotatedString,
577 style: &TextStyle,
578 ) -> TextMetrics {
579 let key = text_base_cache_key(text, style);
580 if let Some(metrics) = self.metrics_cache.borrow().get(&key) {
581 return metrics;
582 }
583 let metrics = self.with_measurer(|m| m.measure_for_node(node_id, text, style));
584 self.metrics_cache.borrow_mut().insert(key, metrics);
585 metrics
586 }
587
588 pub(crate) fn measure_with_options(
589 &self,
590 node_id: Option<NodeId>,
591 text: &crate::text::AnnotatedString,
592 style: &TextStyle,
593 options: TextLayoutOptions,
594 max_width: Option<f32>,
595 ) -> TextMetrics {
596 let key = text_options_cache_key(text, style, options.normalized(), max_width);
597 if let Some(metrics) = self.options_metrics_cache.borrow().get(&key) {
598 return metrics;
599 }
600 let metrics = self.with_measurer(|m| {
601 m.measure_with_options_for_node(node_id, text, style, options.normalized(), max_width)
602 });
603 self.options_metrics_cache.borrow_mut().insert(key, metrics);
604 metrics
605 }
606
607 pub(crate) fn prepare_with_options(
608 &self,
609 node_id: Option<NodeId>,
610 text: &crate::text::AnnotatedString,
611 style: &TextStyle,
612 options: TextLayoutOptions,
613 max_width: Option<f32>,
614 ) -> PreparedTextLayout {
615 let metrics_key = text_options_cache_key(text, style, options.normalized(), max_width);
616 let key = TextPreparedCacheKey {
617 base: metrics_key,
618 visual_hash: style.render_hash(),
619 };
620 if let Some(prepared) = self.prepared_cache.borrow().get(&key) {
621 return prepared;
622 }
623 let prepared = self.with_measurer(|m| {
624 m.prepare_with_options_for_node(node_id, text, style, options.normalized(), max_width)
625 });
626 self.prepared_cache
627 .borrow_mut()
628 .insert(key, prepared.clone());
629 self.options_metrics_cache
630 .borrow_mut()
631 .insert(metrics_key, prepared.metrics);
632 prepared
633 }
634
635 pub(crate) fn layout(
636 &self,
637 text: &crate::text::AnnotatedString,
638 style: &TextStyle,
639 ) -> TextLayoutResult {
640 let key = text_base_cache_key(text, style);
641 if let Some(layout) = self.layout_cache.borrow().get(&key) {
642 return layout;
643 }
644 let layout = self.with_measurer(|m| m.layout(text, style));
645 self.layout_cache.borrow_mut().insert(key, layout.clone());
646 layout
647 }
648
649 fn clear_caches(&self) {
650 self.generation
651 .set(self.generation.get().wrapping_add(1).max(1));
652 self.metrics_cache.borrow_mut().clear();
653 self.options_metrics_cache.borrow_mut().clear();
654 self.prepared_cache.borrow_mut().clear();
655 self.layout_cache.borrow_mut().clear();
656 }
657}
658
659fn text_base_cache_key(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextBaseCacheKey {
660 TextBaseCacheKey {
661 text_hash: text.render_hash(),
662 style_hash: style.measurement_hash(),
663 }
664}
665
666fn text_options_cache_key(
667 text: &crate::text::AnnotatedString,
668 style: &TextStyle,
669 options: TextLayoutOptions,
670 max_width: Option<f32>,
671) -> TextOptionsCacheKey {
672 TextOptionsCacheKey {
673 base: text_base_cache_key(text, style),
674 options: options.normalized(),
675 max_width_bits: normalize_max_width(max_width).map(f32::to_bits),
676 }
677}
678
679pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
680 crate::render_state::set_current_text_measurer(Rc::new(measurer));
681}
682
683pub(crate) fn current_text_generation() -> u64 {
684 crate::render_state::with_text_service(TextService::generation)
685}
686
687pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
688 with_system_font_scale(text, style, |text, style| {
689 crate::render_state::with_text_service(|service| service.measure(None, text, style))
690 })
691}
692
693pub(crate) fn measure_resolved_text(
694 text: &crate::text::AnnotatedString,
695 style: &TextStyle,
696) -> TextMetrics {
697 crate::render_state::with_text_service(|service| service.measure(None, text, style))
698}
699
700pub(crate) fn resolved_first_baseline(style: &TextStyle) -> Option<f32> {
701 crate::render_state::with_text_service(|service| {
702 service.with_measurer(|m| m.first_baseline(style))
703 })
704}
705
706pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
707 crate::render_state::current_app_context()?;
708 crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(style)))
709}
710
711pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
715 let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
716 crate::render_state::with_text_service(|service| {
717 service.with_measurer(|m| m.glyph_line_box(&style))
718 })
719 .map(|(off, h)| (off.min(line_height), h.min(line_height)))
720 .unwrap_or((0.0, line_height))
721}
722
723pub fn first_baseline(style: &TextStyle) -> Option<f32> {
727 let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
728 crate::render_state::with_text_service(|service| {
729 service.with_measurer(|m| m.first_baseline(&style))
730 })
731}
732
733pub fn measure_text_for_node(
734 node_id: Option<NodeId>,
735 text: &crate::text::AnnotatedString,
736 style: &TextStyle,
737) -> TextMetrics {
738 with_system_font_scale(text, style, |text, style| {
739 crate::render_state::with_text_service(|service| service.measure(node_id, text, style))
740 })
741}
742
743pub fn measure_text_with_options(
744 text: &crate::text::AnnotatedString,
745 style: &TextStyle,
746 options: TextLayoutOptions,
747 max_width: Option<f32>,
748) -> TextMetrics {
749 with_system_font_scale(text, style, |text, style| {
750 crate::render_state::with_text_service(|service| {
751 service.measure_with_options(None, text, style, options.normalized(), max_width)
752 })
753 })
754}
755
756pub fn measure_text_with_options_for_node(
757 node_id: Option<NodeId>,
758 text: &crate::text::AnnotatedString,
759 style: &TextStyle,
760 options: TextLayoutOptions,
761 max_width: Option<f32>,
762) -> TextMetrics {
763 with_system_font_scale(text, style, |text, style| {
764 crate::render_state::with_text_service(|service| {
765 service.measure_with_options(node_id, text, style, options.normalized(), max_width)
766 })
767 })
768}
769
770pub fn prepare_text_layout(
771 text: &crate::text::AnnotatedString,
772 style: &TextStyle,
773 options: TextLayoutOptions,
774 max_width: Option<f32>,
775) -> PreparedTextLayout {
776 with_system_font_scale(text, style, |text, style| {
777 crate::render_state::with_text_service(|service| {
778 service.prepare_with_options(None, text, style, options.normalized(), max_width)
779 })
780 })
781}
782
783pub fn prepare_text_layout_for_node(
784 node_id: Option<NodeId>,
785 text: &crate::text::AnnotatedString,
786 style: &TextStyle,
787 options: TextLayoutOptions,
788 max_width: Option<f32>,
789) -> PreparedTextLayout {
790 with_system_font_scale(text, style, |text, style| {
791 crate::render_state::with_text_service(|service| {
792 service.prepare_with_options(node_id, text, style, options.normalized(), max_width)
793 })
794 })
795}
796
797pub fn get_offset_for_position(
798 text: &crate::text::AnnotatedString,
799 style: &TextStyle,
800 x: f32,
801 y: f32,
802) -> usize {
803 with_system_font_scale(text, style, |text, style| {
804 crate::render_state::with_text_measurer(|m| m.get_offset_for_position(text, style, x, y))
805 })
806}
807
808pub fn offset_for_position_wrapped(
822 text: &str,
823 style: &TextStyle,
824 node_id: Option<NodeId>,
825 wrap_width: Option<f32>,
826 line_height: f32,
827 x: f32,
828 y: f32,
829) -> usize {
830 if text.is_empty() {
831 return 0;
832 }
833 let annotated = crate::text::AnnotatedString::from(text);
834 let line_ranges = wrapped_line_ranges(
835 node_id,
836 &annotated,
837 style,
838 TextLayoutOptions::default(),
839 wrap_width,
840 );
841 if line_ranges.is_empty() {
842 return 0;
843 }
844 let line_idx = if line_height > 0.0 {
845 (y / line_height).floor().max(0.0) as usize
846 } else {
847 0
848 }
849 .min(line_ranges.len() - 1);
850 let range = &line_ranges[line_idx];
851 let line = &text[range.start..range.end];
852 let within = get_offset_for_position(&crate::text::AnnotatedString::from(line), style, x, 0.0);
853 range.start + within.min(line.len())
854}
855
856pub fn get_cursor_x_for_offset(
857 text: &crate::text::AnnotatedString,
858 style: &TextStyle,
859 offset: usize,
860) -> f32 {
861 with_system_font_scale(text, style, |text, style| {
862 crate::render_state::with_text_measurer(|m| m.get_cursor_x_for_offset(text, style, offset))
863 })
864}
865
866pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
867 with_system_font_scale(text, style, |text, style| {
868 crate::render_state::with_text_service(|service| service.layout(text, style))
869 })
870}
871
872pub fn wrapped_line_ranges(
885 node_id: Option<NodeId>,
886 text: &crate::text::AnnotatedString,
887 style: &TextStyle,
888 options: TextLayoutOptions,
889 max_width: Option<f32>,
890) -> Vec<Range<usize>> {
891 with_system_font_scale(text, style, |text, style| {
892 crate::render_state::with_text_measurer(|m| {
893 wrapped_line_ranges_with_measurer(m, node_id, text, style, options, max_width)
894 })
895 })
896}
897
898fn wrapped_line_ranges_with_measurer<M: TextMeasurer + ?Sized>(
899 measurer: &M,
900 _node_id: Option<NodeId>,
901 text: &crate::text::AnnotatedString,
902 style: &TextStyle,
903 options: TextLayoutOptions,
904 max_width: Option<f32>,
905) -> Vec<Range<usize>> {
906 let opts = options.normalized();
907 let max_width = normalize_max_width(max_width);
908 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
909 .then_some(max_width)
910 .flatten();
911 let line_break_mode = style
912 .paragraph_style
913 .line_break
914 .take_or_else(|| LineBreak::Simple);
915 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
916
917 let line_ranges = split_line_ranges(text.text.as_str());
918 let Some(width_limit) = wrap_width else {
919 return line_ranges;
920 };
921 let mut ranges = Vec::with_capacity(line_ranges.len());
922 for line_range in line_ranges {
923 for display_line in wrap_line_to_width(
924 measurer,
925 text,
926 line_range,
927 style,
928 width_limit,
929 line_break_mode,
930 hyphens_mode,
931 ) {
932 ranges.push(display_line.source_range.clone());
933 }
934 }
935 ranges
936}
937
938fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
939 measurer: &M,
940 text: &crate::text::AnnotatedString,
941 style: &TextStyle,
942 options: TextLayoutOptions,
943 max_width: Option<f32>,
944) -> PreparedTextLayout {
945 prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
946}
947
948pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
949 measurer: &M,
950 node_id: Option<NodeId>,
951 text: &crate::text::AnnotatedString,
952 style: &TextStyle,
953 options: TextLayoutOptions,
954 max_width: Option<f32>,
955) -> PreparedTextLayout {
956 let telemetry = text_layout_telemetry_enabled();
957 let total_start = telemetry.then(Instant::now);
958 let opts = options.normalized();
959 let max_width = normalize_max_width(max_width);
960 if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
961 return prepare_scale_down_text_layout(
962 measurer,
963 node_id,
964 text,
965 style,
966 opts,
967 max_width,
968 min_font_size_sp,
969 );
970 }
971
972 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
973 .then_some(max_width)
974 .flatten();
975 let line_break_mode = style
976 .paragraph_style
977 .line_break
978 .take_or_else(|| LineBreak::Simple);
979 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
980
981 let wrap_start = telemetry.then(Instant::now);
982 let line_ranges = split_line_ranges(text.text.as_str());
983 let source_line_count = line_ranges.len();
984 let mut visible_lines: Vec<DisplayLine>;
985 if let Some(width_limit) = wrap_width {
986 visible_lines = Vec::with_capacity(line_ranges.len());
987 for line_range in line_ranges {
988 let wrapped_lines = wrap_line_to_width(
989 measurer,
990 text,
991 line_range,
992 style,
993 width_limit,
994 line_break_mode,
995 hyphens_mode,
996 );
997 visible_lines.extend(wrapped_lines);
998 }
999 } else {
1000 visible_lines = line_ranges
1001 .into_iter()
1002 .map(DisplayLine::from_source_range)
1003 .collect();
1004 }
1005 let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1006
1007 let overflow_start = telemetry.then(Instant::now);
1008 let mut did_overflow = false;
1009 if opts.overflow != TextOverflow::Visible && visible_lines.len() > opts.max_lines {
1010 did_overflow = true;
1011 visible_lines.truncate(opts.max_lines);
1012 if let Some(last_line) = visible_lines.last_mut() {
1013 let overflowed = apply_line_overflow(
1014 measurer,
1015 last_line.display_text(text),
1016 style,
1017 max_width,
1018 opts,
1019 true,
1020 true,
1021 );
1022 last_line.apply_display_text(text, overflowed);
1023 }
1024 }
1025
1026 if let Some(width_limit) = max_width {
1027 let single_line_ellipsis = opts.max_lines == 1 || !opts.soft_wrap;
1028 let visible_len = visible_lines.len();
1029 for (line_index, line) in visible_lines.iter_mut().enumerate() {
1030 let width = line.measure_width(measurer, node_id, text, style);
1031 if width > width_limit + WRAP_EPSILON {
1032 if opts.overflow == TextOverflow::Visible {
1033 continue;
1034 }
1035 did_overflow = true;
1036 let overflowed = apply_line_overflow(
1037 measurer,
1038 line.display_text(text),
1039 style,
1040 Some(width_limit),
1041 opts,
1042 line_index + 1 == visible_len,
1043 single_line_ellipsis,
1044 );
1045 line.apply_display_text(text, overflowed);
1046 }
1047 }
1048 }
1049 let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1050
1051 let build_start = telemetry.then(Instant::now);
1052 let display_annotated = build_display_annotated(text, &visible_lines);
1053 debug_assert_eq!(
1054 display_annotated.text,
1055 join_display_line_text(text, &visible_lines)
1056 );
1057 let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1058
1059 let metrics_start = telemetry.then(Instant::now);
1060 let line_height = measurer.line_height_for_node(node_id, text, style).max(0.0);
1061 let display_line_count = visible_lines.len().max(1);
1062 let layout_line_count = display_line_count.max(opts.min_lines);
1063
1064 let measured_width = if visible_lines.is_empty() {
1065 0.0
1066 } else {
1067 visible_lines
1068 .iter()
1069 .map(|line| line.measure_width(measurer, node_id, text, style))
1070 .fold(0.0_f32, f32::max)
1071 };
1072 let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1073 let width = if opts.overflow == TextOverflow::Visible {
1074 measured_width
1075 } else if let Some(width_limit) = max_width {
1076 measured_width.min(width_limit)
1077 } else {
1078 measured_width
1079 };
1080
1081 let prepared = PreparedTextLayout {
1082 text: display_annotated,
1083 visual_style: style.clone(),
1084 metrics: TextMetrics {
1085 width,
1086 height: layout_line_count as f32 * line_height,
1087 line_height,
1088 line_count: layout_line_count,
1089 },
1090 did_overflow,
1091 };
1092
1093 if let Some(start) = total_start {
1094 eprintln!(
1095 "[text-layout-telemetry] bytes={} spans={} source_lines={} display_lines={} wrap={} max_width={:?} wrap_ms={:.2} overflow_ms={:.2} build_ms={:.2} metrics_ms={:.2} total_ms={:.2}",
1096 text.text.len(),
1097 text.span_styles.len(),
1098 source_line_count,
1099 display_line_count,
1100 wrap_width.is_some(),
1101 max_width,
1102 wrap_ms.unwrap_or(0.0),
1103 overflow_ms.unwrap_or(0.0),
1104 build_ms.unwrap_or(0.0),
1105 metrics_ms.unwrap_or(0.0),
1106 start.elapsed().as_secs_f64() * 1000.0,
1107 );
1108 }
1109
1110 prepared
1111}
1112
1113fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1114 measurer: &M,
1115 node_id: Option<NodeId>,
1116 text: &crate::text::AnnotatedString,
1117 style: &TextStyle,
1118 options: TextLayoutOptions,
1119 max_width: Option<f32>,
1120 min_font_size_sp: f32,
1121) -> PreparedTextLayout {
1122 let clipped_options = TextLayoutOptions {
1123 overflow: TextOverflow::Clip,
1124 ..options
1125 }
1126 .normalized();
1127
1128 let full_size = prepare_scaled_text_layout(
1129 measurer,
1130 node_id,
1131 text,
1132 style,
1133 clipped_options,
1134 max_width,
1135 FontScaleCurve::linear(1.0),
1136 );
1137 let Some(width_limit) = max_width else {
1138 return full_size;
1139 };
1140 if !full_size.did_overflow {
1141 return full_size;
1142 }
1143
1144 let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1145 if !base_font_size.is_finite() || base_font_size <= 0.0 {
1146 return full_size;
1147 }
1148 let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1149 if min_scale >= 1.0 {
1150 return full_size;
1151 }
1152
1153 let min_size = prepare_scaled_text_layout(
1154 measurer,
1155 node_id,
1156 text,
1157 style,
1158 clipped_options,
1159 Some(width_limit),
1160 FontScaleCurve::linear(min_scale),
1161 );
1162 if min_size.did_overflow {
1163 return min_size;
1164 }
1165
1166 let mut low = min_scale;
1167 let mut high = 1.0;
1168 let mut best = min_size;
1169 for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1170 let mid = (low + high) * 0.5;
1171 let candidate = prepare_scaled_text_layout(
1172 measurer,
1173 node_id,
1174 text,
1175 style,
1176 clipped_options,
1177 Some(width_limit),
1178 FontScaleCurve::linear(mid),
1179 );
1180 if candidate.did_overflow {
1181 high = mid;
1182 } else {
1183 low = mid;
1184 best = candidate;
1185 }
1186 }
1187
1188 best
1189}
1190
1191fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1192 measurer: &M,
1193 node_id: Option<NodeId>,
1194 text: &crate::text::AnnotatedString,
1195 style: &TextStyle,
1196 options: TextLayoutOptions,
1197 max_width: Option<f32>,
1198 shrink: FontScaleCurve,
1199) -> PreparedTextLayout {
1200 let visual_style = scale_text_style_font_sizes(style, shrink);
1201 let visual_text = scale_annotated_font_sizes(text, shrink);
1202 prepare_text_layout_with_measurer_for_node(
1203 measurer,
1204 node_id,
1205 visual_text.as_ref(),
1206 &visual_style,
1207 options,
1208 max_width,
1209 )
1210}
1211
1212fn scale_annotated_font_sizes(
1213 text: &crate::text::AnnotatedString,
1214 curve: FontScaleCurve,
1215) -> Cow<'_, crate::text::AnnotatedString> {
1216 if curve.is_identity() || !annotated_text_needs_scaling(text) {
1217 return Cow::Borrowed(text);
1218 }
1219
1220 let mut scaled = text.clone();
1221 for span in &mut scaled.span_styles {
1222 span.item = scale_span_style_font_sizes(&span.item, curve, None);
1223 }
1224 Cow::Owned(scaled)
1225}
1226
1227fn scale_text_style_font_sizes(style: &TextStyle, curve: FontScaleCurve) -> TextStyle {
1228 if curve.is_identity() {
1229 return style.clone();
1230 }
1231
1232 let mut scaled = style.clone();
1233 scaled.span_style =
1234 scale_span_style_font_sizes(&style.span_style, curve, Some(DEFAULT_FONT_SIZE_SP));
1235 scaled.paragraph_style.line_height =
1236 scale_text_unit_sp(scaled.paragraph_style.line_height, curve);
1237 if let Some(mut indent) = scaled.paragraph_style.text_indent {
1238 indent.first_line = scale_text_unit_sp(indent.first_line, curve);
1239 indent.rest_line = scale_text_unit_sp(indent.rest_line, curve);
1240 scaled.paragraph_style.text_indent = Some(indent);
1241 }
1242 scaled
1243}
1244
1245fn with_system_font_scale<R>(
1246 text: &crate::text::AnnotatedString,
1247 style: &TextStyle,
1248 block: impl FnOnce(&crate::text::AnnotatedString, &TextStyle) -> R,
1249) -> R {
1250 let curve = crate::current_font_scale_curve();
1251 let visual_style = scale_text_style_font_sizes(style, curve);
1252 let visual_text = scale_annotated_font_sizes(text, curve);
1253 block(visual_text.as_ref(), &visual_style)
1254}
1255
1256fn scale_span_style_font_sizes(
1257 style: &crate::text::SpanStyle,
1258 curve: FontScaleCurve,
1259 default_font_size_sp: Option<f32>,
1260) -> crate::text::SpanStyle {
1261 let factor = curve.scale();
1262 let mut scaled = style.clone();
1263 scaled.font_size = match (style.font_size, default_font_size_sp) {
1264 (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1265 crate::text::TextUnit::Sp(curve.sp_to_dp(default_size))
1266 }
1267 (unit, Some(_)) => scale_text_unit_sp_and_em(unit, curve),
1268 (unit, None) => scale_text_unit_sp(unit, curve),
1269 };
1270 scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, curve);
1271 if let Some(mut shadow) = scaled.shadow {
1272 shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1273 shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1274 shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1275 scaled.shadow = Some(shadow);
1276 }
1277 if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1278 scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1279 width: width * factor,
1280 });
1281 }
1282 scaled
1283}
1284
1285fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1286 text.span_styles
1287 .iter()
1288 .any(|span| span_style_needs_scaling(&span.item))
1289}
1290
1291fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1292 matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1293 || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1294 || matches!(
1295 style.draw_style,
1296 Some(crate::text::TextDrawStyle::Stroke { .. })
1297 )
1298 || style.shadow.is_some()
1299}
1300
1301fn scale_text_unit_sp(unit: crate::text::TextUnit, curve: FontScaleCurve) -> crate::text::TextUnit {
1302 match unit {
1303 crate::text::TextUnit::Sp(value) if value.is_finite() => {
1304 crate::text::TextUnit::Sp(curve.sp_to_dp(value))
1305 }
1306 other => other,
1307 }
1308}
1309
1310fn scale_text_unit_sp_and_em(
1311 unit: crate::text::TextUnit,
1312 curve: FontScaleCurve,
1313) -> crate::text::TextUnit {
1314 match unit {
1315 crate::text::TextUnit::Sp(_) => scale_text_unit_sp(unit, curve),
1316 crate::text::TextUnit::Em(value) if value.is_finite() => {
1317 crate::text::TextUnit::Em(value * curve.scale())
1318 }
1319 other => other,
1320 }
1321}
1322
1323fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1324 if value.is_finite() {
1325 value * factor
1326 } else {
1327 value
1328 }
1329}
1330
1331#[derive(Clone, Debug)]
1332enum DisplayLineText {
1333 Source,
1334 Remapped(crate::text::AnnotatedString),
1335}
1336
1337#[derive(Clone, Debug)]
1338struct DisplayLine {
1339 source_range: Range<usize>,
1340 text: DisplayLineText,
1341 measured_width: Option<f32>,
1342}
1343
1344impl DisplayLine {
1345 fn from_source_range(source_range: Range<usize>) -> Self {
1346 Self {
1347 source_range,
1348 text: DisplayLineText::Source,
1349 measured_width: None,
1350 }
1351 }
1352
1353 fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1354 Self {
1355 source_range,
1356 text: DisplayLineText::Source,
1357 measured_width: measured_width
1358 .is_finite()
1359 .then_some(measured_width.max(0.0)),
1360 }
1361 }
1362
1363 fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1364 match &self.text {
1365 DisplayLineText::Source => &source.text[self.source_range.clone()],
1366 DisplayLineText::Remapped(annotated) => annotated.text.as_str(),
1367 }
1368 }
1369
1370 fn measure_width<M: TextMeasurer + ?Sized>(
1371 &self,
1372 measurer: &M,
1373 node_id: Option<NodeId>,
1374 source: &crate::text::AnnotatedString,
1375 style: &TextStyle,
1376 ) -> f32 {
1377 match &self.text {
1378 DisplayLineText::Source => self.measured_width.unwrap_or_else(|| {
1379 measurer
1380 .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1381 .width
1382 }),
1383 DisplayLineText::Remapped(annotated) => {
1384 measurer.measure_for_node(node_id, annotated, style).width
1385 }
1386 }
1387 }
1388
1389 fn apply_display_text(&mut self, source: &crate::text::AnnotatedString, display_text: String) {
1390 let source_text = &source.text[self.source_range.clone()];
1391 self.measured_width = None;
1392 self.text = if source_text == display_text {
1393 DisplayLineText::Source
1394 } else {
1395 DisplayLineText::Remapped(remap_annotated_subsequence_for_display(
1396 source,
1397 self.source_range.clone(),
1398 display_text.as_str(),
1399 ))
1400 };
1401 }
1402}
1403
1404fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1405 if text.is_empty() {
1406 return single_line_range(0..0);
1407 }
1408
1409 let mut ranges = Vec::new();
1410 let mut start = 0usize;
1411 for (idx, ch) in text.char_indices() {
1412 if ch == '\n' {
1413 ranges.push(start..idx);
1414 start = idx + ch.len_utf8();
1415 }
1416 }
1417 ranges.push(start..text.len());
1418 ranges
1419}
1420
1421fn build_display_annotated(
1422 source: &crate::text::AnnotatedString,
1423 lines: &[DisplayLine],
1424) -> crate::text::AnnotatedString {
1425 if lines.is_empty() {
1426 return crate::text::AnnotatedString::from("");
1427 }
1428
1429 let mut builder = crate::text::AnnotatedString::builder();
1430 for (idx, line) in lines.iter().enumerate() {
1431 builder = match &line.text {
1432 DisplayLineText::Source => {
1433 builder.append_annotated_subsequence(source, line.source_range.clone())
1434 }
1435 DisplayLineText::Remapped(annotated) => builder.append_annotated(annotated),
1436 };
1437 if idx + 1 < lines.len() {
1438 builder = builder.append("\n");
1439 }
1440 }
1441 builder.to_annotated_string()
1442}
1443
1444fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1445 let mut text = String::new();
1446 for (idx, line) in lines.iter().enumerate() {
1447 text.push_str(line.display_text(source));
1448 if idx + 1 < lines.len() {
1449 text.push('\n');
1450 }
1451 }
1452 text
1453}
1454
1455fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1456 while end > start {
1457 let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1458 break;
1459 };
1460 if ch.is_whitespace() {
1461 end = start + idx;
1462 } else {
1463 break;
1464 }
1465 }
1466 end
1467}
1468
1469fn remap_annotated_subsequence_for_display(
1470 source: &crate::text::AnnotatedString,
1471 source_range: Range<usize>,
1472 display_text: &str,
1473) -> crate::text::AnnotatedString {
1474 let source_text = &source.text[source_range.clone()];
1475 if source_text == display_text {
1476 return source.subsequence(source_range);
1477 }
1478
1479 let display_chars = map_display_chars_to_source(source_text, display_text);
1480 crate::text::AnnotatedString {
1481 text: display_text.to_string(),
1482 span_styles: remap_subsequence_range_styles(
1483 &source.span_styles,
1484 source_range.clone(),
1485 &display_chars,
1486 ),
1487 paragraph_styles: remap_subsequence_range_styles(
1488 &source.paragraph_styles,
1489 source_range.clone(),
1490 &display_chars,
1491 ),
1492 string_annotations: remap_subsequence_range_styles(
1493 &source.string_annotations,
1494 source_range.clone(),
1495 &display_chars,
1496 ),
1497 link_annotations: remap_subsequence_range_styles(
1498 &source.link_annotations,
1499 source_range,
1500 &display_chars,
1501 ),
1502 }
1503}
1504
1505#[derive(Clone, Copy)]
1506struct DisplayCharMap {
1507 display_start: usize,
1508 display_end: usize,
1509 source_start: Option<usize>,
1510}
1511
1512fn map_display_chars_to_source(source: &str, display: &str) -> Vec<DisplayCharMap> {
1513 let source_chars: Vec<(usize, char)> = source.char_indices().collect();
1514 let mut source_index = 0usize;
1515 let mut maps = Vec::with_capacity(display.chars().count());
1516
1517 for (display_start, display_char) in display.char_indices() {
1518 let display_end = display_start + display_char.len_utf8();
1519 let mut source_start = None;
1520 while source_index < source_chars.len() {
1521 let (candidate_start, candidate_char) = source_chars[source_index];
1522 source_index += 1;
1523 if candidate_char == display_char {
1524 source_start = Some(candidate_start);
1525 break;
1526 }
1527 }
1528 maps.push(DisplayCharMap {
1529 display_start,
1530 display_end,
1531 source_start,
1532 });
1533 }
1534
1535 maps
1536}
1537
1538fn remap_subsequence_range_styles<T: Clone>(
1539 styles: &[crate::text::RangeStyle<T>],
1540 source_range: Range<usize>,
1541 display_chars: &[DisplayCharMap],
1542) -> Vec<crate::text::RangeStyle<T>> {
1543 let mut remapped = Vec::new();
1544
1545 for style in styles {
1546 let overlap_start = style.range.start.max(source_range.start);
1547 let overlap_end = style.range.end.min(source_range.end);
1548 if overlap_start >= overlap_end {
1549 continue;
1550 }
1551 let local_source_range =
1552 (overlap_start - source_range.start)..(overlap_end - source_range.start);
1553 let mut range_start = None;
1554 let mut range_end = 0usize;
1555
1556 for map in display_chars {
1557 let in_range = map.source_start.is_some_and(|source_start| {
1558 source_start >= local_source_range.start && source_start < local_source_range.end
1559 });
1560
1561 if in_range {
1562 if range_start.is_none() {
1563 range_start = Some(map.display_start);
1564 }
1565 range_end = map.display_end;
1566 continue;
1567 }
1568
1569 if let Some(start) = range_start.take()
1570 && start < range_end
1571 {
1572 remapped.push(crate::text::RangeStyle {
1573 item: style.item.clone(),
1574 range: start..range_end,
1575 });
1576 }
1577 }
1578
1579 if let Some(start) = range_start.take()
1580 && start < range_end
1581 {
1582 remapped.push(crate::text::RangeStyle {
1583 item: style.item.clone(),
1584 range: start..range_end,
1585 });
1586 }
1587 }
1588
1589 remapped
1590}
1591
1592fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1593 match max_width {
1594 Some(width) if width.is_finite() && width > 0.0 => Some(width),
1595 _ => None,
1596 }
1597}
1598
1599fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1600 (base_start + relative.start)..(base_start + relative.end)
1601}
1602
1603fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1604 boundaries
1605 .binary_search(&byte_offset)
1606 .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1607}
1608
1609fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1610 std::iter::once(range).collect()
1611}
1612
1613struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1614 measurer: &'a M,
1615 text: &'a crate::text::AnnotatedString,
1616 style: &'a TextStyle,
1617 line_start: usize,
1618 prefix_widths: Option<TextLinePrefixWidths>,
1619}
1620
1621impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1622 fn new(
1623 measurer: &'a M,
1624 text: &'a crate::text::AnnotatedString,
1625 line_range: &Range<usize>,
1626 style: &'a TextStyle,
1627 boundary_count: usize,
1628 ) -> Self {
1629 let expected_chars = boundary_count.saturating_sub(1);
1630 let prefix_widths = measurer
1631 .measure_line_prefix_widths(text, line_range.clone(), style)
1632 .filter(|widths| widths.char_count() == expected_chars);
1633 Self {
1634 measurer,
1635 text,
1636 style,
1637 line_start: line_range.start,
1638 prefix_widths,
1639 }
1640 }
1641
1642 fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1643 if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1644 return width;
1645 }
1646 let segment_range =
1647 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1648 self.measurer
1649 .measure_subsequence(self.text, segment_range, self.style)
1650 .width
1651 }
1652
1653 fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1654 if let Some(prefix_widths) = &self.prefix_widths
1655 && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1656 {
1657 return Some(width);
1658 }
1659 None
1660 }
1661
1662 fn display_line_for_char_range(
1663 &self,
1664 boundaries: &[usize],
1665 start_idx: usize,
1666 end_idx: usize,
1667 ) -> DisplayLine {
1668 let source_range =
1669 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1670 let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1671 DisplayLine::from_measured_source_range(source_range, measured_width)
1672 }
1673}
1674
1675fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1676 measurer: &M,
1677 text: &crate::text::AnnotatedString,
1678 line_range: Range<usize>,
1679 style: &TextStyle,
1680 max_width: f32,
1681 line_break: LineBreak,
1682 hyphens: Hyphens,
1683) -> Vec<DisplayLine> {
1684 let line_text = &text.text[line_range.clone()];
1685 if line_text.is_empty() {
1686 return vec![DisplayLine::from_source_range(
1687 line_range.start..line_range.start,
1688 )];
1689 }
1690
1691 if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
1692 && measured_width <= max_width + WRAP_EPSILON
1693 {
1694 return vec![DisplayLine::from_measured_source_range(
1695 line_range,
1696 measured_width,
1697 )];
1698 }
1699
1700 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1701 && line_text.chars().any(char::is_whitespace)
1702 && let Some(balanced) = wrap_line_with_word_balance(
1703 measurer,
1704 text,
1705 line_range.clone(),
1706 style,
1707 max_width,
1708 line_break,
1709 )
1710 {
1711 return balanced;
1712 }
1713
1714 wrap_line_greedy(
1715 measurer, text, line_range, style, max_width, line_break, hyphens,
1716 )
1717}
1718
1719fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1720 measurer: &M,
1721 text: &crate::text::AnnotatedString,
1722 line_range: Range<usize>,
1723 style: &TextStyle,
1724 max_width: f32,
1725 line_break: LineBreak,
1726 hyphens: Hyphens,
1727) -> Vec<DisplayLine> {
1728 let line_text = &text.text[line_range.clone()];
1729 let boundaries = char_boundaries(line_text);
1730 let measure_context =
1731 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1732 if let Some(measured_width) =
1733 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1734 && measured_width <= max_width + WRAP_EPSILON
1735 {
1736 return vec![DisplayLine::from_measured_source_range(
1737 line_range,
1738 measured_width,
1739 )];
1740 }
1741 let mut wrapped = Vec::new();
1742 let mut start_idx = 0usize;
1743
1744 while start_idx < boundaries.len() - 1 {
1745 let mut low = start_idx + 1;
1746 let mut high = boundaries.len() - 1;
1747 let mut best = start_idx + 1;
1748
1749 while low <= high {
1750 let mid = (low + high) / 2;
1751 let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1752 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1753 best = mid;
1754 low = mid + 1;
1755 } else {
1756 if mid == 0 {
1757 break;
1758 }
1759 high = mid - 1;
1760 }
1761 }
1762
1763 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1764 let mut effective_wrap_idx = wrap_idx;
1765 let can_hyphenate = hyphens == Hyphens::Auto
1766 && wrap_idx == best
1767 && best < boundaries.len() - 1
1768 && is_break_inside_word(line_text, &boundaries, wrap_idx);
1769 if can_hyphenate {
1770 effective_wrap_idx = resolve_auto_hyphen_break(
1771 measurer,
1772 line_text,
1773 style,
1774 &boundaries,
1775 start_idx,
1776 wrap_idx,
1777 );
1778 }
1779
1780 let broke_at_word_boundary = effective_wrap_idx > start_idx
1781 && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
1782 .chars()
1783 .all(char::is_whitespace);
1784 let segment_start = boundaries[start_idx];
1785 let mut segment_end = boundaries[effective_wrap_idx];
1786 if wrap_idx != best || broke_at_word_boundary {
1787 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1788 }
1789 let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1790 wrapped.push(measure_context.display_line_for_char_range(
1791 &boundaries,
1792 start_idx,
1793 segment_end_idx,
1794 ));
1795
1796 start_idx = if wrap_idx != best || broke_at_word_boundary {
1797 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1798 } else {
1799 effective_wrap_idx
1800 };
1801 }
1802
1803 if wrapped.is_empty() {
1804 wrapped.push(DisplayLine::from_source_range(
1805 line_range.start..line_range.start,
1806 ));
1807 }
1808
1809 wrapped
1810}
1811
1812fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1813 measurer: &M,
1814 text: &crate::text::AnnotatedString,
1815 line_range: Range<usize>,
1816 style: &TextStyle,
1817 max_width: f32,
1818 line_break: LineBreak,
1819) -> Option<Vec<DisplayLine>> {
1820 let line_text = &text.text[line_range.clone()];
1821 let boundaries = char_boundaries(line_text);
1822 let measure_context =
1823 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1824 if let Some(measured_width) =
1825 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1826 && measured_width <= max_width + WRAP_EPSILON
1827 {
1828 return Some(vec![DisplayLine::from_measured_source_range(
1829 line_range,
1830 measured_width,
1831 )]);
1832 }
1833 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1834 if breakpoints.len() <= 2 {
1835 return None;
1836 }
1837
1838 let node_count = breakpoints.len();
1839 let mut best_cost = vec![f32::INFINITY; node_count];
1840 let mut next_index = vec![None; node_count];
1841 best_cost[node_count - 1] = 0.0;
1842
1843 for start in (0..node_count - 1).rev() {
1844 for end in start + 1..node_count {
1845 let start_byte = boundaries[breakpoints[start]];
1846 let end_byte = boundaries[breakpoints[end]];
1847 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1848 if trimmed_end <= start_byte {
1849 continue;
1850 }
1851 let segment_start_idx = breakpoints[start];
1852 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1853 let segment_width =
1854 measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1855 if segment_width > max_width + WRAP_EPSILON {
1856 continue;
1857 }
1858 if !best_cost[end].is_finite() {
1859 continue;
1860 }
1861 let slack = (max_width - segment_width).max(0.0);
1862 let is_last = end == node_count - 1;
1863 let segment_cost = match line_break {
1864 LineBreak::Heading => slack * slack,
1865 LineBreak::Paragraph => {
1866 if is_last {
1867 slack * slack * 0.16
1868 } else {
1869 slack * slack
1870 }
1871 }
1872 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1873 };
1874 let candidate = segment_cost + best_cost[end];
1875 if candidate < best_cost[start] {
1876 best_cost[start] = candidate;
1877 next_index[start] = Some(end);
1878 }
1879 }
1880 }
1881
1882 let mut wrapped = Vec::new();
1883 let mut current = 0usize;
1884 while current < node_count - 1 {
1885 let next = next_index[current]?;
1886 let start_byte = boundaries[breakpoints[current]];
1887 let end_byte = boundaries[breakpoints[next]];
1888 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1889 if trimmed_end <= start_byte {
1890 return None;
1891 }
1892 let segment_start_idx = breakpoints[current];
1893 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1894 wrapped.push(measure_context.display_line_for_char_range(
1895 &boundaries,
1896 segment_start_idx,
1897 segment_end_idx,
1898 ));
1899 current = next;
1900 }
1901
1902 if wrapped.is_empty() {
1903 return None;
1904 }
1905
1906 Some(wrapped)
1907}
1908
1909fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1910 let mut points = vec![0usize];
1911 for idx in 1..boundaries.len() - 1 {
1912 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1913 let current = &line[boundaries[idx]..boundaries[idx + 1]];
1914 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1915 points.push(idx);
1916 }
1917 }
1918 let end = boundaries.len() - 1;
1919 if points.last().copied() != Some(end) {
1920 points.push(end);
1921 }
1922 points
1923}
1924
1925fn choose_wrap_break(
1926 line: &str,
1927 boundaries: &[usize],
1928 start_idx: usize,
1929 best: usize,
1930 _line_break: LineBreak,
1931) -> usize {
1932 if best >= boundaries.len() - 1 {
1933 return best;
1934 }
1935
1936 if best <= start_idx + 1 {
1937 return best;
1938 }
1939
1940 for idx in (start_idx + 1..=best).rev() {
1941 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1942 if prev.chars().all(char::is_whitespace) {
1943 return idx;
1944 }
1945 }
1946 best
1947}
1948
1949fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1950 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1951 return false;
1952 }
1953 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1954 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1955 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1956}
1957
1958fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1959 measurer: &M,
1960 line: &str,
1961 style: &TextStyle,
1962 boundaries: &[usize],
1963 start_idx: usize,
1964 break_idx: usize,
1965) -> usize {
1966 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
1967 && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
1968 {
1969 return candidate;
1970 }
1971 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1972}
1973
1974fn is_valid_auto_hyphen_break(
1975 line: &str,
1976 boundaries: &[usize],
1977 start_idx: usize,
1978 break_idx: usize,
1979 candidate_idx: usize,
1980) -> bool {
1981 let end_idx = boundaries.len().saturating_sub(1);
1982 candidate_idx > start_idx
1983 && candidate_idx < end_idx
1984 && candidate_idx <= break_idx
1985 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1986 && is_break_inside_word(line, boundaries, candidate_idx)
1987}
1988
1989fn choose_auto_hyphen_break_fallback(
1990 boundaries: &[usize],
1991 start_idx: usize,
1992 break_idx: usize,
1993) -> usize {
1994 let end_idx = boundaries.len().saturating_sub(1);
1995 if break_idx >= end_idx {
1996 return break_idx;
1997 }
1998 let trailing_len = end_idx.saturating_sub(break_idx);
1999 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
2000 return break_idx;
2001 }
2002
2003 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
2004 let max_break = break_idx.saturating_sub(1);
2005 if min_break > max_break {
2006 return break_idx;
2007 }
2008
2009 let mut best_break = break_idx;
2010 let mut best_penalty = usize::MAX;
2011 for idx in min_break..=max_break {
2012 let candidate_trailing_len = end_idx.saturating_sub(idx);
2013 let candidate_prefix_len = idx.saturating_sub(start_idx);
2014 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
2015 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
2016 {
2017 continue;
2018 }
2019
2020 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
2021 if penalty < best_penalty {
2022 best_penalty = penalty;
2023 best_break = idx;
2024 if penalty == 0 {
2025 break;
2026 }
2027 }
2028 }
2029 best_break
2030}
2031
2032fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
2033 while idx < boundaries.len() - 1 {
2034 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
2035 if !ch.chars().all(char::is_whitespace) {
2036 break;
2037 }
2038 idx += 1;
2039 }
2040 idx
2041}
2042
2043fn apply_line_overflow<M: TextMeasurer + ?Sized>(
2044 measurer: &M,
2045 line: &str,
2046 style: &TextStyle,
2047 max_width: Option<f32>,
2048 options: TextLayoutOptions,
2049 is_last_visible_line: bool,
2050 single_line_ellipsis: bool,
2051) -> String {
2052 if options.overflow == TextOverflow::Clip || !is_last_visible_line {
2053 return line.to_string();
2054 }
2055
2056 let Some(width_limit) = max_width else {
2057 return match options.overflow {
2058 TextOverflow::Ellipsis => format!("{line}{ELLIPSIS}"),
2059 TextOverflow::StartEllipsis => format!("{ELLIPSIS}{line}"),
2060 TextOverflow::MiddleEllipsis => format!("{ELLIPSIS}{line}"),
2061 TextOverflow::Clip | TextOverflow::Visible | TextOverflow::ScaleDown { .. } => {
2062 line.to_string()
2063 }
2064 };
2065 };
2066
2067 match options.overflow {
2068 TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
2069 TextOverflow::Ellipsis => fit_end_ellipsis(measurer, line, style, width_limit),
2070 TextOverflow::StartEllipsis => {
2071 if single_line_ellipsis {
2072 fit_start_ellipsis(measurer, line, style, width_limit)
2073 } else {
2074 line.to_string()
2075 }
2076 }
2077 TextOverflow::MiddleEllipsis => {
2078 if single_line_ellipsis {
2079 fit_middle_ellipsis(measurer, line, style, width_limit)
2080 } else {
2081 line.to_string()
2082 }
2083 }
2084 TextOverflow::ScaleDown { .. } => line.to_string(),
2085 }
2086}
2087
2088fn measured_width<M: TextMeasurer + ?Sized>(measurer: &M, text: &str, style: &TextStyle) -> f32 {
2089 measurer
2090 .measure(&crate::text::AnnotatedString::from(text), style)
2091 .width
2092}
2093
2094enum EllipsisTruncation {
2095 NotNeeded,
2096 ImpossibleFit,
2097 Truncate(Vec<usize>),
2098}
2099
2100fn ellipsis_truncation<M: TextMeasurer + ?Sized>(
2101 measurer: &M,
2102 line: &str,
2103 style: &TextStyle,
2104 max_width: f32,
2105) -> EllipsisTruncation {
2106 if measured_width(measurer, line, style) <= max_width + WRAP_EPSILON {
2107 return EllipsisTruncation::NotNeeded;
2108 }
2109 if measured_width(measurer, ELLIPSIS, style) > max_width + WRAP_EPSILON {
2110 return EllipsisTruncation::ImpossibleFit;
2111 }
2112 EllipsisTruncation::Truncate(char_boundaries(line))
2113}
2114
2115fn fit_end_ellipsis<M: TextMeasurer + ?Sized>(
2116 measurer: &M,
2117 line: &str,
2118 style: &TextStyle,
2119 max_width: f32,
2120) -> String {
2121 let boundaries = match ellipsis_truncation(measurer, line, style, max_width) {
2122 EllipsisTruncation::NotNeeded => return line.to_string(),
2123 EllipsisTruncation::ImpossibleFit => return String::new(),
2124 EllipsisTruncation::Truncate(boundaries) => boundaries,
2125 };
2126
2127 let mut low = 0usize;
2128 let mut high = boundaries.len() - 1;
2129 let mut best = 0usize;
2130
2131 while low <= high {
2132 let mid = (low + high) / 2;
2133 let prefix = &line[..boundaries[mid]];
2134 let candidate = format!("{prefix}{ELLIPSIS}");
2135 if measured_width(measurer, &candidate, style) <= max_width + WRAP_EPSILON {
2136 best = mid;
2137 low = mid + 1;
2138 } else if mid == 0 {
2139 break;
2140 } else {
2141 high = mid - 1;
2142 }
2143 }
2144
2145 format!("{}{}", &line[..boundaries[best]], ELLIPSIS)
2146}
2147
2148fn fit_start_ellipsis<M: TextMeasurer + ?Sized>(
2149 measurer: &M,
2150 line: &str,
2151 style: &TextStyle,
2152 max_width: f32,
2153) -> String {
2154 let boundaries = match ellipsis_truncation(measurer, line, style, max_width) {
2155 EllipsisTruncation::NotNeeded => return line.to_string(),
2156 EllipsisTruncation::ImpossibleFit => return String::new(),
2157 EllipsisTruncation::Truncate(boundaries) => boundaries,
2158 };
2159
2160 let mut low = 0usize;
2161 let mut high = boundaries.len() - 1;
2162 let mut best = boundaries.len() - 1;
2163
2164 while low <= high {
2165 let mid = (low + high) / 2;
2166 let suffix = &line[boundaries[mid]..];
2167 let candidate = format!("{ELLIPSIS}{suffix}");
2168 if measured_width(measurer, &candidate, style) <= max_width + WRAP_EPSILON {
2169 best = mid;
2170 if mid == 0 {
2171 break;
2172 }
2173 high = mid - 1;
2174 } else {
2175 low = mid + 1;
2176 }
2177 }
2178
2179 format!("{ELLIPSIS}{}", &line[boundaries[best]..])
2180}
2181
2182fn fit_middle_ellipsis<M: TextMeasurer + ?Sized>(
2183 measurer: &M,
2184 line: &str,
2185 style: &TextStyle,
2186 max_width: f32,
2187) -> String {
2188 let boundaries = match ellipsis_truncation(measurer, line, style, max_width) {
2189 EllipsisTruncation::NotNeeded => return line.to_string(),
2190 EllipsisTruncation::ImpossibleFit => return String::new(),
2191 EllipsisTruncation::Truncate(boundaries) => boundaries,
2192 };
2193
2194 let total_chars = boundaries.len().saturating_sub(1);
2195 for keep in (0..=total_chars).rev() {
2196 let keep_start = keep.div_ceil(2);
2197 let keep_end = keep / 2;
2198 let start = &line[..boundaries[keep_start]];
2199 let end_start = boundaries[total_chars.saturating_sub(keep_end)];
2200 let end = &line[end_start..];
2201 let candidate = format!("{start}{ELLIPSIS}{end}");
2202 if measured_width(measurer, &candidate, style) <= max_width + WRAP_EPSILON {
2203 return candidate;
2204 }
2205 }
2206
2207 ELLIPSIS.to_string()
2208}
2209
2210fn char_boundaries(text: &str) -> Vec<usize> {
2211 let mut out = Vec::with_capacity(text.chars().count() + 1);
2212 out.push(0);
2213 for (idx, _) in text.char_indices() {
2214 if idx != 0 {
2215 out.push(idx);
2216 }
2217 }
2218 out.push(text.len());
2219 out
2220}
2221
2222#[cfg(test)]
2223mod tests {
2224 use std::cell::Cell;
2225
2226 use super::*;
2227 use crate::{
2228 text::{Hyphens, LineBreak, ParagraphStyle, TextUnit},
2229 text_layout_result::TextLayoutResult,
2230 };
2231
2232 #[test]
2233 fn text_layout_telemetry_env_flag_is_not_process_cached() {
2234 let source = include_str!("measure.rs");
2235 let once_lock = ["Once", "Lock"].concat();
2236 let cached_init_call = ["get", "_or", "_init"].concat();
2237
2238 assert!(
2239 !source.contains(&once_lock) && !source.contains(&cached_init_call),
2240 "text layout telemetry env flag must be read at the diagnostic boundary"
2241 );
2242 }
2243
2244 #[test]
2245 fn prepared_layout_cache_distinguishes_visual_styles() {
2246 let service = TextService::new();
2247 let text = crate::text::AnnotatedString::from("tinted".to_string());
2248 let options = TextLayoutOptions::default();
2249
2250 let mut style = TextStyle::default();
2251 style.span_style.color = Some(crate::Color(1.0, 0.0, 0.0, 1.0));
2252 let red = service.prepare_with_options(None, &text, &style, options, None);
2253
2254 style.span_style.color = Some(crate::Color(0.0, 0.0, 1.0, 1.0));
2255 let blue = service.prepare_with_options(None, &text, &style, options, None);
2256
2257 assert_eq!(
2258 red.visual_style.span_style.color,
2259 Some(crate::Color(1.0, 0.0, 0.0, 1.0)),
2260 );
2261 assert_eq!(
2262 blue.visual_style.span_style.color,
2263 Some(crate::Color(0.0, 0.0, 1.0, 1.0)),
2264 "a color-only style change must not be served a stale prepared layout \
2265 (measurement hashes ignore visual attributes by design)"
2266 );
2267 }
2268
2269 #[test]
2270 fn system_font_scale_changes_sp_measurement_and_prepared_text() {
2271 let _app_context = crate::render_state::app_context_test_scope();
2272 let text = crate::text::AnnotatedString::from("scale me");
2273 let style = TextStyle {
2274 span_style: crate::text::SpanStyle {
2275 font_size: TextUnit::Sp(10.0),
2276 ..Default::default()
2277 },
2278 ..Default::default()
2279 };
2280
2281 let unscaled = measure_text(&text, &style);
2282 crate::set_font_scale(2.0);
2283 let scaled = measure_text(&text, &style);
2284 let prepared = prepare_text_layout(&text, &style, TextLayoutOptions::default(), None);
2285
2286 assert!((scaled.width - unscaled.width * 2.0).abs() <= f32::EPSILON);
2287 assert!((scaled.height - unscaled.height * 2.0).abs() <= f32::EPSILON);
2288 assert_eq!(
2289 prepared.visual_style.span_style.font_size,
2290 TextUnit::Sp(20.0)
2291 );
2292 }
2293
2294 #[test]
2295 fn a_platform_curve_resolves_an_sp_where_the_platform_does_and_not_where_a_multiplier_would() {
2296 let _app_context = crate::render_state::app_context_test_scope();
2297 let text = crate::text::AnnotatedString::from("SOLID next at 3 gold");
2298 let style = TextStyle {
2299 span_style: crate::text::SpanStyle {
2300 font_size: TextUnit::Sp(13.0),
2301 letter_spacing: TextUnit::Sp(0.4),
2302 ..Default::default()
2303 },
2304 ..Default::default()
2305 };
2306
2307 crate::set_font_scale_curve(FontScaleCurve::from_samples(
2308 1.24,
2309 &[
2310 (8.0, 9.92),
2311 (10.0, 12.4),
2312 (12.0, 14.88),
2313 (14.0, 17.84),
2314 (16.0, 19.36),
2315 (18.0, 20.88),
2316 (20.0, 22.88),
2317 (24.0, 25.92),
2318 (30.0, 30.0),
2319 (100.0, 100.0),
2320 ],
2321 ));
2322 let prepared = prepare_text_layout(&text, &style, TextLayoutOptions::default(), None);
2323 assert_eq!(
2324 prepared.visual_style.span_style.font_size,
2325 TextUnit::Sp(16.36)
2326 );
2327 assert_eq!(
2328 prepared.visual_style.span_style.letter_spacing,
2329 TextUnit::Sp(0.4 * 1.24)
2330 );
2331 assert_eq!(crate::current_font_scale(), 1.24);
2332
2333 crate::set_font_scale(1.24);
2334 let multiplied = prepare_text_layout(&text, &style, TextLayoutOptions::default(), None);
2335 assert_eq!(
2336 multiplied.visual_style.span_style.font_size,
2337 TextUnit::Sp(13.0 * 1.24)
2338 );
2339 }
2340
2341 #[test]
2342 fn text_service_cache_retains_large_lazy_text_working_set() {
2343 let mut cache = BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY);
2344 let metrics = TextMetrics {
2345 width: 1.0,
2346 height: 1.0,
2347 line_height: 1.0,
2348 line_count: 1,
2349 };
2350
2351 for index in 0..4096u64 {
2352 cache.insert(
2353 TextBaseCacheKey {
2354 text_hash: index,
2355 style_hash: 7,
2356 },
2357 metrics,
2358 );
2359 }
2360
2361 for index in 0..4096u64 {
2362 assert!(
2363 cache
2364 .get(&TextBaseCacheKey {
2365 text_hash: index,
2366 style_hash: 7,
2367 })
2368 .is_some(),
2369 "large lazy text working-set entry {index} was evicted too early"
2370 );
2371 }
2372 }
2373
2374 struct ContractBreakMeasurer {
2375 retreat: usize,
2376 }
2377
2378 impl TextMeasurer for ContractBreakMeasurer {
2379 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2380 MonospacedTextMeasurer.measure(
2381 &crate::text::AnnotatedString::from(text.text.as_str()),
2382 style,
2383 )
2384 }
2385
2386 fn get_offset_for_position(
2387 &self,
2388 text: &crate::text::AnnotatedString,
2389 style: &TextStyle,
2390 x: f32,
2391 y: f32,
2392 ) -> usize {
2393 MonospacedTextMeasurer.get_offset_for_position(
2394 &crate::text::AnnotatedString::from(text.text.as_str()),
2395 style,
2396 x,
2397 y,
2398 )
2399 }
2400
2401 fn get_cursor_x_for_offset(
2402 &self,
2403 text: &crate::text::AnnotatedString,
2404 style: &TextStyle,
2405 offset: usize,
2406 ) -> f32 {
2407 MonospacedTextMeasurer.get_cursor_x_for_offset(
2408 &crate::text::AnnotatedString::from(text.text.as_str()),
2409 style,
2410 offset,
2411 )
2412 }
2413
2414 fn layout(
2415 &self,
2416 text: &crate::text::AnnotatedString,
2417 style: &TextStyle,
2418 ) -> TextLayoutResult {
2419 MonospacedTextMeasurer.layout(
2420 &crate::text::AnnotatedString::from(text.text.as_str()),
2421 style,
2422 )
2423 }
2424
2425 fn choose_auto_hyphen_break(
2426 &self,
2427 _line: &str,
2428 _style: &TextStyle,
2429 _segment_start_char: usize,
2430 measured_break_char: usize,
2431 ) -> Option<usize> {
2432 measured_break_char.checked_sub(self.retreat)
2433 }
2434 }
2435
2436 fn monospaced_measure(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2437 MonospacedTextMeasurer.measure(text, style)
2438 }
2439
2440 fn monospaced_offset_for_position(
2441 text: &crate::text::AnnotatedString,
2442 style: &TextStyle,
2443 x: f32,
2444 y: f32,
2445 ) -> usize {
2446 MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2447 }
2448
2449 fn monospaced_cursor_x_for_offset(
2450 text: &crate::text::AnnotatedString,
2451 style: &TextStyle,
2452 offset: usize,
2453 ) -> f32 {
2454 MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2455 }
2456
2457 fn monospaced_layout(
2458 text: &crate::text::AnnotatedString,
2459 style: &TextStyle,
2460 ) -> TextLayoutResult {
2461 MonospacedTextMeasurer.layout(text, style)
2462 }
2463
2464 struct CountingTextMeasurer {
2465 measure_calls: Rc<Cell<usize>>,
2466 layout_calls: Rc<Cell<usize>>,
2467 }
2468
2469 impl CountingTextMeasurer {
2470 fn new(measure_calls: Rc<Cell<usize>>, layout_calls: Rc<Cell<usize>>) -> Self {
2471 Self {
2472 measure_calls,
2473 layout_calls,
2474 }
2475 }
2476 }
2477
2478 impl TextMeasurer for CountingTextMeasurer {
2479 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2480 self.measure_calls.set(self.measure_calls.get() + 1);
2481 monospaced_measure(text, style)
2482 }
2483
2484 fn get_offset_for_position(
2485 &self,
2486 text: &crate::text::AnnotatedString,
2487 style: &TextStyle,
2488 x: f32,
2489 y: f32,
2490 ) -> usize {
2491 monospaced_offset_for_position(text, style, x, y)
2492 }
2493
2494 fn get_cursor_x_for_offset(
2495 &self,
2496 text: &crate::text::AnnotatedString,
2497 style: &TextStyle,
2498 offset: usize,
2499 ) -> f32 {
2500 monospaced_cursor_x_for_offset(text, style, offset)
2501 }
2502
2503 fn layout(
2504 &self,
2505 text: &crate::text::AnnotatedString,
2506 style: &TextStyle,
2507 ) -> TextLayoutResult {
2508 self.layout_calls.set(self.layout_calls.get() + 1);
2509 monospaced_layout(text, style)
2510 }
2511 }
2512
2513 struct CountingPreparedTextMeasurer {
2514 prepare_calls: Rc<Cell<usize>>,
2515 }
2516
2517 impl CountingPreparedTextMeasurer {
2518 fn new(prepare_calls: Rc<Cell<usize>>) -> Self {
2519 Self { prepare_calls }
2520 }
2521 }
2522
2523 struct PrefixWidthCountingMeasurer {
2524 prefix_calls: Rc<Cell<usize>>,
2525 subsequence_calls: Rc<Cell<usize>>,
2526 }
2527
2528 impl PrefixWidthCountingMeasurer {
2529 fn new(prefix_calls: Rc<Cell<usize>>, subsequence_calls: Rc<Cell<usize>>) -> Self {
2530 Self {
2531 prefix_calls,
2532 subsequence_calls,
2533 }
2534 }
2535 }
2536
2537 impl TextMeasurer for PrefixWidthCountingMeasurer {
2538 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2539 monospaced_measure(text, style)
2540 }
2541
2542 fn measure_subsequence(
2543 &self,
2544 text: &crate::text::AnnotatedString,
2545 range: Range<usize>,
2546 style: &TextStyle,
2547 ) -> TextMetrics {
2548 self.subsequence_calls.set(self.subsequence_calls.get() + 1);
2549 MonospacedTextMeasurer.measure_subsequence(text, range, style)
2550 }
2551
2552 fn measure_line_prefix_widths(
2553 &self,
2554 text: &crate::text::AnnotatedString,
2555 line_range: Range<usize>,
2556 style: &TextStyle,
2557 ) -> Option<TextLinePrefixWidths> {
2558 self.prefix_calls.set(self.prefix_calls.get() + 1);
2559 MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2560 }
2561
2562 fn get_offset_for_position(
2563 &self,
2564 text: &crate::text::AnnotatedString,
2565 style: &TextStyle,
2566 x: f32,
2567 y: f32,
2568 ) -> usize {
2569 monospaced_offset_for_position(text, style, x, y)
2570 }
2571
2572 fn get_cursor_x_for_offset(
2573 &self,
2574 text: &crate::text::AnnotatedString,
2575 style: &TextStyle,
2576 offset: usize,
2577 ) -> f32 {
2578 monospaced_cursor_x_for_offset(text, style, offset)
2579 }
2580
2581 fn layout(
2582 &self,
2583 text: &crate::text::AnnotatedString,
2584 style: &TextStyle,
2585 ) -> TextLayoutResult {
2586 monospaced_layout(text, style)
2587 }
2588 }
2589
2590 struct LineHeightCountingMeasurer {
2591 measure_calls: Rc<Cell<usize>>,
2592 line_height_calls: Rc<Cell<usize>>,
2593 }
2594
2595 struct FitProbeCountingMeasurer {
2596 line_width_calls: Rc<Cell<usize>>,
2597 prefix_calls: Rc<Cell<usize>>,
2598 }
2599
2600 impl FitProbeCountingMeasurer {
2601 fn new(line_width_calls: Rc<Cell<usize>>, prefix_calls: Rc<Cell<usize>>) -> Self {
2602 Self {
2603 line_width_calls,
2604 prefix_calls,
2605 }
2606 }
2607 }
2608
2609 impl LineHeightCountingMeasurer {
2610 fn new(measure_calls: Rc<Cell<usize>>, line_height_calls: Rc<Cell<usize>>) -> Self {
2611 Self {
2612 measure_calls,
2613 line_height_calls,
2614 }
2615 }
2616 }
2617
2618 impl TextMeasurer for LineHeightCountingMeasurer {
2619 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2620 self.measure_calls.set(self.measure_calls.get() + 1);
2621 monospaced_measure(text, style)
2622 }
2623
2624 fn measure_line_prefix_widths(
2625 &self,
2626 text: &crate::text::AnnotatedString,
2627 line_range: Range<usize>,
2628 style: &TextStyle,
2629 ) -> Option<TextLinePrefixWidths> {
2630 MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2631 }
2632
2633 fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
2634 self.line_height_calls.set(self.line_height_calls.get() + 1);
2635 MonospacedTextMeasurer.line_height(text, style)
2636 }
2637
2638 fn get_offset_for_position(
2639 &self,
2640 text: &crate::text::AnnotatedString,
2641 style: &TextStyle,
2642 x: f32,
2643 y: f32,
2644 ) -> usize {
2645 monospaced_offset_for_position(text, style, x, y)
2646 }
2647
2648 fn get_cursor_x_for_offset(
2649 &self,
2650 text: &crate::text::AnnotatedString,
2651 style: &TextStyle,
2652 offset: usize,
2653 ) -> f32 {
2654 monospaced_cursor_x_for_offset(text, style, offset)
2655 }
2656
2657 fn layout(
2658 &self,
2659 text: &crate::text::AnnotatedString,
2660 style: &TextStyle,
2661 ) -> TextLayoutResult {
2662 monospaced_layout(text, style)
2663 }
2664 }
2665
2666 impl TextMeasurer for FitProbeCountingMeasurer {
2667 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2668 monospaced_measure(text, style)
2669 }
2670
2671 fn measure_line_width(
2672 &self,
2673 text: &crate::text::AnnotatedString,
2674 line_range: Range<usize>,
2675 style: &TextStyle,
2676 ) -> Option<f32> {
2677 self.line_width_calls.set(self.line_width_calls.get() + 1);
2678 MonospacedTextMeasurer.measure_line_width(text, line_range, style)
2679 }
2680
2681 fn measure_line_prefix_widths(
2682 &self,
2683 text: &crate::text::AnnotatedString,
2684 line_range: Range<usize>,
2685 style: &TextStyle,
2686 ) -> Option<TextLinePrefixWidths> {
2687 self.prefix_calls.set(self.prefix_calls.get() + 1);
2688 MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2689 }
2690
2691 fn get_offset_for_position(
2692 &self,
2693 text: &crate::text::AnnotatedString,
2694 style: &TextStyle,
2695 x: f32,
2696 y: f32,
2697 ) -> usize {
2698 monospaced_offset_for_position(text, style, x, y)
2699 }
2700
2701 fn get_cursor_x_for_offset(
2702 &self,
2703 text: &crate::text::AnnotatedString,
2704 style: &TextStyle,
2705 offset: usize,
2706 ) -> f32 {
2707 monospaced_cursor_x_for_offset(text, style, offset)
2708 }
2709
2710 fn layout(
2711 &self,
2712 text: &crate::text::AnnotatedString,
2713 style: &TextStyle,
2714 ) -> TextLayoutResult {
2715 monospaced_layout(text, style)
2716 }
2717 }
2718
2719 impl TextMeasurer for CountingPreparedTextMeasurer {
2720 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2721 monospaced_measure(text, style)
2722 }
2723
2724 fn prepare_with_options_for_node(
2725 &self,
2726 _node_id: Option<NodeId>,
2727 text: &crate::text::AnnotatedString,
2728 style: &TextStyle,
2729 options: TextLayoutOptions,
2730 max_width: Option<f32>,
2731 ) -> PreparedTextLayout {
2732 self.prepare_calls.set(self.prepare_calls.get() + 1);
2733 MonospacedTextMeasurer.prepare_with_options(text, style, options, max_width)
2734 }
2735
2736 fn get_offset_for_position(
2737 &self,
2738 text: &crate::text::AnnotatedString,
2739 style: &TextStyle,
2740 x: f32,
2741 y: f32,
2742 ) -> usize {
2743 monospaced_offset_for_position(text, style, x, y)
2744 }
2745
2746 fn get_cursor_x_for_offset(
2747 &self,
2748 text: &crate::text::AnnotatedString,
2749 style: &TextStyle,
2750 offset: usize,
2751 ) -> f32 {
2752 monospaced_cursor_x_for_offset(text, style, offset)
2753 }
2754
2755 fn layout(
2756 &self,
2757 text: &crate::text::AnnotatedString,
2758 style: &TextStyle,
2759 ) -> TextLayoutResult {
2760 monospaced_layout(text, style)
2761 }
2762 }
2763
2764 #[test]
2765 fn text_service_routes_measurement_through_current_measurer() {
2766 let _app_context = crate::render_state::app_context_test_scope();
2767 let service = TextService::from_measurer(Rc::new(MonospacedTextMeasurer));
2768 let text = crate::text::AnnotatedString::from("abc");
2769 let style = TextStyle::default();
2770
2771 let metrics = service.with_measurer(|measurer| measurer.measure(&text, &style));
2772
2773 assert!(metrics.width > 0.0);
2774 assert!(metrics.height > 0.0);
2775 }
2776
2777 #[test]
2778 fn text_service_caches_metrics_and_layouts_per_context() {
2779 let _app_context = crate::render_state::app_context_test_scope();
2780 let measure_calls = Rc::new(Cell::new(0));
2781 let layout_calls = Rc::new(Cell::new(0));
2782 let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2783 Rc::clone(&measure_calls),
2784 Rc::clone(&layout_calls),
2785 )));
2786 let text = crate::text::AnnotatedString::from("cached text");
2787 let style = TextStyle::default();
2788
2789 let first_metrics = service.measure(Some(7), &text, &style);
2790 let second_metrics = service.measure(Some(7), &text, &style);
2791 let first_layout = service.layout(&text, &style);
2792 let second_layout = service.layout(&text, &style);
2793
2794 assert_eq!(first_metrics, second_metrics);
2795 assert_eq!(first_layout.width, second_layout.width);
2796 assert_eq!(measure_calls.get(), 1);
2797 assert_eq!(layout_calls.get(), 1);
2798 }
2799
2800 #[test]
2801 fn text_service_reuses_metrics_cache_across_node_ids() {
2802 let _app_context = crate::render_state::app_context_test_scope();
2803 let measure_calls = Rc::new(Cell::new(0));
2804 let layout_calls = Rc::new(Cell::new(0));
2805 let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2806 Rc::clone(&measure_calls),
2807 Rc::clone(&layout_calls),
2808 )));
2809 let text = crate::text::AnnotatedString::from("same lazy item text");
2810 let style = TextStyle::default();
2811
2812 let first_metrics = service.measure(Some(7), &text, &style);
2813 let second_metrics = service.measure(Some(8), &text, &style);
2814
2815 assert_eq!(first_metrics, second_metrics);
2816 assert_eq!(measure_calls.get(), 1);
2817 }
2818
2819 #[test]
2820 fn text_service_reuses_prepared_layout_cache_across_node_ids() {
2821 let _app_context = crate::render_state::app_context_test_scope();
2822 let prepare_calls = Rc::new(Cell::new(0));
2823 let service = TextService::from_measurer(Rc::new(CountingPreparedTextMeasurer::new(
2824 Rc::clone(&prepare_calls),
2825 )));
2826 let text = crate::text::AnnotatedString::from("same prepared lazy item text");
2827 let style = TextStyle::default();
2828 let options = TextLayoutOptions::default();
2829
2830 let first = service.prepare_with_options(Some(9), &text, &style, options, Some(120.0));
2831 let second = service.prepare_with_options(Some(10), &text, &style, options, Some(120.0));
2832
2833 assert_eq!(first.metrics, second.metrics);
2834 assert_eq!(prepare_calls.get(), 1);
2835 }
2836
2837 #[test]
2838 fn text_service_clears_caches_when_measurer_changes() {
2839 let _app_context = crate::render_state::app_context_test_scope();
2840 let first_measure_calls = Rc::new(Cell::new(0));
2841 let second_measure_calls = Rc::new(Cell::new(0));
2842 let layout_calls = Rc::new(Cell::new(0));
2843 let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2844 Rc::clone(&first_measure_calls),
2845 Rc::clone(&layout_calls),
2846 )));
2847 let text = crate::text::AnnotatedString::from("cached text");
2848 let style = TextStyle::default();
2849
2850 let _ = service.measure(None, &text, &style);
2851 let _ = service.measure(None, &text, &style);
2852 service.set_measurer(Rc::new(CountingTextMeasurer::new(
2853 Rc::clone(&second_measure_calls),
2854 Rc::clone(&layout_calls),
2855 )));
2856 let _ = service.measure(None, &text, &style);
2857
2858 assert_eq!(first_measure_calls.get(), 1);
2859 assert_eq!(second_measure_calls.get(), 1);
2860 }
2861
2862 #[test]
2863 fn text_wrapping_uses_prefix_widths_without_subsequence_measurement() {
2864 let _app_context = crate::render_state::app_context_test_scope();
2865 let prefix_calls = Rc::new(Cell::new(0));
2866 let subsequence_calls = Rc::new(Cell::new(0));
2867 set_text_measurer(PrefixWidthCountingMeasurer::new(
2868 Rc::clone(&prefix_calls),
2869 Rc::clone(&subsequence_calls),
2870 ));
2871 let style = TextStyle {
2872 span_style: crate::text::SpanStyle {
2873 font_size: TextUnit::Sp(10.0),
2874 ..Default::default()
2875 },
2876 ..Default::default()
2877 };
2878 let options = TextLayoutOptions {
2879 overflow: TextOverflow::Clip,
2880 soft_wrap: true,
2881 max_lines: usize::MAX,
2882 min_lines: 1,
2883 };
2884 let text = crate::text::AnnotatedString::from("word ".repeat(80).as_str());
2885
2886 let prepared = prepare_text_layout(&text, &style, options, Some(80.0));
2887
2888 assert!(prepared.metrics.line_count > 1);
2889 assert!(
2890 prefix_calls.get() > 0,
2891 "wrapping should request a line prefix width plan"
2892 );
2893 assert_eq!(
2894 subsequence_calls.get(),
2895 0,
2896 "prefix-capable wrapping should not probe candidate substrings"
2897 );
2898 }
2899
2900 #[test]
2901 fn text_wrapping_skips_prefix_widths_when_fit_probe_says_line_fits() {
2902 let _app_context = crate::render_state::app_context_test_scope();
2903 let line_width_calls = Rc::new(Cell::new(0));
2904 let prefix_calls = Rc::new(Cell::new(0));
2905 set_text_measurer(FitProbeCountingMeasurer::new(
2906 Rc::clone(&line_width_calls),
2907 Rc::clone(&prefix_calls),
2908 ));
2909 let style = TextStyle {
2910 span_style: crate::text::SpanStyle {
2911 font_size: TextUnit::Sp(10.0),
2912 ..Default::default()
2913 },
2914 ..Default::default()
2915 };
2916 let text = crate::text::AnnotatedString::from("fits without per-glyph prefix widths");
2917
2918 let prepared =
2919 prepare_text_layout(&text, &style, TextLayoutOptions::default(), Some(800.0));
2920
2921 assert_eq!(prepared.metrics.line_count, 1);
2922 assert_eq!(line_width_calls.get(), 1);
2923 assert_eq!(
2924 prefix_calls.get(),
2925 0,
2926 "fitting lines should not allocate prefix-width plans"
2927 );
2928 }
2929
2930 #[test]
2931 fn prepare_text_layout_uses_line_height_without_full_text_measurement() {
2932 let _app_context = crate::render_state::app_context_test_scope();
2933 let measure_calls = Rc::new(Cell::new(0));
2934 let line_height_calls = Rc::new(Cell::new(0));
2935 let measurer = LineHeightCountingMeasurer::new(
2936 Rc::clone(&measure_calls),
2937 Rc::clone(&line_height_calls),
2938 );
2939 let text = crate::text::AnnotatedString::from(
2940 "one two three four five six seven eight nine ten eleven twelve",
2941 );
2942
2943 let prepared = prepare_text_layout_with_measurer_for_node(
2944 &measurer,
2945 Some(7),
2946 &text,
2947 &TextStyle::default(),
2948 TextLayoutOptions::default(),
2949 Some(96.0),
2950 );
2951
2952 assert!(prepared.metrics.height > 0.0);
2953 assert_eq!(line_height_calls.get(), 1);
2954 assert_eq!(
2955 measure_calls.get(),
2956 0,
2957 "line-height lookup must not re-measure the whole paragraph"
2958 );
2959 }
2960
2961 fn style_with_line_break(line_break: LineBreak) -> TextStyle {
2962 TextStyle {
2963 span_style: crate::text::SpanStyle {
2964 font_size: TextUnit::Sp(10.0),
2965 ..Default::default()
2966 },
2967 paragraph_style: ParagraphStyle {
2968 line_break,
2969 ..Default::default()
2970 },
2971 }
2972 }
2973
2974 fn style_with_hyphens(hyphens: Hyphens) -> TextStyle {
2975 TextStyle {
2976 span_style: crate::text::SpanStyle {
2977 font_size: TextUnit::Sp(10.0),
2978 ..Default::default()
2979 },
2980 paragraph_style: ParagraphStyle {
2981 hyphens,
2982 ..Default::default()
2983 },
2984 }
2985 }
2986
2987 fn assert_f32_close(actual: f32, expected: f32) {
2988 assert!(
2989 (actual - expected).abs() <= 0.01,
2990 "actual={actual}, expected={expected}"
2991 );
2992 }
2993
2994 #[test]
2995 fn text_layout_options_wraps_and_limits_lines() {
2996 let _app_context = crate::render_state::app_context_test_scope();
2997 let style = TextStyle {
2998 span_style: crate::text::SpanStyle {
2999 font_size: TextUnit::Sp(10.0),
3000 ..Default::default()
3001 },
3002 ..Default::default()
3003 };
3004 let options = TextLayoutOptions {
3005 overflow: TextOverflow::Clip,
3006 soft_wrap: true,
3007 max_lines: 2,
3008 min_lines: 1,
3009 };
3010
3011 let prepared = prepare_text_layout(
3012 &crate::text::AnnotatedString::from("A B C D E F"),
3013 &style,
3014 options,
3015 Some(24.0),
3016 );
3017
3018 assert!(prepared.did_overflow);
3019 assert!(prepared.metrics.line_count <= 2);
3020 }
3021
3022 #[test]
3023 fn text_layout_options_end_ellipsis_applies() {
3024 let _app_context = crate::render_state::app_context_test_scope();
3025 let style = TextStyle {
3026 span_style: crate::text::SpanStyle {
3027 font_size: TextUnit::Sp(10.0),
3028 ..Default::default()
3029 },
3030 ..Default::default()
3031 };
3032 let options = TextLayoutOptions {
3033 overflow: TextOverflow::Ellipsis,
3034 soft_wrap: false,
3035 max_lines: 1,
3036 min_lines: 1,
3037 };
3038
3039 let prepared = prepare_text_layout(
3040 &crate::text::AnnotatedString::from("Long long line"),
3041 &style,
3042 options,
3043 Some(20.0),
3044 );
3045 assert!(prepared.did_overflow);
3046 assert!(prepared.text.text.contains(ELLIPSIS));
3047 }
3048
3049 #[test]
3050 fn text_layout_options_visible_keeps_full_text() {
3051 let _app_context = crate::render_state::app_context_test_scope();
3052 let style = TextStyle {
3053 span_style: crate::text::SpanStyle {
3054 font_size: TextUnit::Sp(10.0),
3055 ..Default::default()
3056 },
3057 ..Default::default()
3058 };
3059 let options = TextLayoutOptions {
3060 overflow: TextOverflow::Visible,
3061 soft_wrap: false,
3062 max_lines: 1,
3063 min_lines: 1,
3064 };
3065
3066 let input = "This should remain unchanged";
3067 let prepared = prepare_text_layout(
3068 &crate::text::AnnotatedString::from(input),
3069 &style,
3070 options,
3071 Some(10.0),
3072 );
3073 assert_eq!(prepared.text.text, input);
3074 }
3075
3076 #[test]
3077 fn text_layout_options_respects_min_lines() {
3078 let _app_context = crate::render_state::app_context_test_scope();
3079 let style = TextStyle {
3080 span_style: crate::text::SpanStyle {
3081 font_size: TextUnit::Sp(10.0),
3082 ..Default::default()
3083 },
3084 ..Default::default()
3085 };
3086 let options = TextLayoutOptions {
3087 overflow: TextOverflow::Clip,
3088 soft_wrap: true,
3089 max_lines: 4,
3090 min_lines: 3,
3091 };
3092
3093 let prepared = prepare_text_layout(
3094 &crate::text::AnnotatedString::from("short"),
3095 &style,
3096 options,
3097 Some(100.0),
3098 );
3099 assert_eq!(prepared.metrics.line_count, 3);
3100 }
3101
3102 #[test]
3103 fn text_layout_options_middle_ellipsis_for_single_line() {
3104 let _app_context = crate::render_state::app_context_test_scope();
3105 let style = TextStyle {
3106 span_style: crate::text::SpanStyle {
3107 font_size: TextUnit::Sp(10.0),
3108 ..Default::default()
3109 },
3110 ..Default::default()
3111 };
3112 let options = TextLayoutOptions {
3113 overflow: TextOverflow::MiddleEllipsis,
3114 soft_wrap: false,
3115 max_lines: 1,
3116 min_lines: 1,
3117 };
3118
3119 let prepared = prepare_text_layout(
3120 &crate::text::AnnotatedString::from("abcdefghijk"),
3121 &style,
3122 options,
3123 Some(24.0),
3124 );
3125 assert!(prepared.text.text.contains(ELLIPSIS));
3126 assert!(prepared.did_overflow);
3127 }
3128
3129 #[test]
3130 fn text_layout_options_scale_down_fits_without_rewriting_text() {
3131 let _app_context = crate::render_state::app_context_test_scope();
3132 let style = TextStyle {
3133 span_style: crate::text::SpanStyle {
3134 font_size: TextUnit::Sp(20.0),
3135 ..Default::default()
3136 },
3137 ..Default::default()
3138 };
3139 let options = TextLayoutOptions {
3140 overflow: TextOverflow::ScaleDown {
3141 min_font_size_sp: 10.0,
3142 },
3143 soft_wrap: false,
3144 max_lines: 1,
3145 min_lines: 1,
3146 };
3147
3148 let prepared = prepare_text_layout(
3149 &crate::text::AnnotatedString::from("ABCDE"),
3150 &style,
3151 options,
3152 Some(36.0),
3153 );
3154
3155 assert_eq!(prepared.text.text, "ABCDE");
3156 assert!(prepared.metrics.width <= 36.0 + WRAP_EPSILON);
3157 assert!(!prepared.did_overflow);
3158 let visual_font_size = prepared.visual_style.resolve_font_size(14.0);
3159 assert!(visual_font_size < 20.0);
3160 assert!(visual_font_size >= 10.0);
3161 }
3162
3163 #[test]
3164 fn text_layout_options_scale_down_scales_root_shadow() {
3165 let _app_context = crate::render_state::app_context_test_scope();
3166 let style = TextStyle {
3167 span_style: crate::text::SpanStyle {
3168 font_size: TextUnit::Sp(20.0),
3169 shadow: Some(crate::text::Shadow {
3170 color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
3171 offset: crate::modifier::Point::new(8.0, 4.0),
3172 blur_radius: 6.0,
3173 }),
3174 ..Default::default()
3175 },
3176 ..Default::default()
3177 };
3178 let options = TextLayoutOptions {
3179 overflow: TextOverflow::ScaleDown {
3180 min_font_size_sp: 10.0,
3181 },
3182 soft_wrap: false,
3183 max_lines: 1,
3184 min_lines: 1,
3185 };
3186
3187 let prepared = prepare_text_layout(
3188 &crate::text::AnnotatedString::from("ABCDE"),
3189 &style,
3190 options,
3191 Some(36.0),
3192 );
3193
3194 let font_scale = prepared.visual_style.resolve_font_size(14.0) / 20.0;
3195 let shadow = prepared
3196 .visual_style
3197 .span_style
3198 .shadow
3199 .expect("scaled style should retain shadow");
3200 assert_f32_close(shadow.offset.x, 8.0 * font_scale);
3201 assert_f32_close(shadow.offset.y, 4.0 * font_scale);
3202 assert_f32_close(shadow.blur_radius, 6.0 * font_scale);
3203 }
3204
3205 #[test]
3206 fn text_layout_options_scale_down_stops_at_minimum_and_clips() {
3207 let _app_context = crate::render_state::app_context_test_scope();
3208 let style = TextStyle {
3209 span_style: crate::text::SpanStyle {
3210 font_size: TextUnit::Sp(20.0),
3211 ..Default::default()
3212 },
3213 ..Default::default()
3214 };
3215 let options = TextLayoutOptions {
3216 overflow: TextOverflow::ScaleDown {
3217 min_font_size_sp: 10.0,
3218 },
3219 soft_wrap: false,
3220 max_lines: 1,
3221 min_lines: 1,
3222 };
3223
3224 let prepared = prepare_text_layout(
3225 &crate::text::AnnotatedString::from("ABCDEFGHIJ"),
3226 &style,
3227 options,
3228 Some(12.0),
3229 );
3230
3231 assert_eq!(prepared.text.text, "ABCDEFGHIJ");
3232 assert!(prepared.did_overflow);
3233 assert_eq!(prepared.metrics.width, 12.0);
3234 assert_eq!(prepared.visual_style.resolve_font_size(14.0), 10.0);
3235 }
3236
3237 #[test]
3238 fn scale_annotated_font_sizes_borrows_when_spans_need_no_scaling() {
3239 let _app_context = crate::render_state::app_context_test_scope();
3240 let plain = crate::text::AnnotatedString::from("plain");
3241 assert!(matches!(
3242 scale_annotated_font_sizes(&plain, FontScaleCurve::linear(0.5)),
3243 std::borrow::Cow::Borrowed(_)
3244 ));
3245
3246 let colored = crate::text::annotated_string::Builder::new()
3247 .push_style(crate::text::SpanStyle {
3248 color: Some(crate::modifier::Color(1.0, 0.0, 0.0, 1.0)),
3249 ..Default::default()
3250 })
3251 .append("colored")
3252 .pop()
3253 .to_annotated_string();
3254 assert!(matches!(
3255 scale_annotated_font_sizes(&colored, FontScaleCurve::linear(0.5)),
3256 std::borrow::Cow::Borrowed(_)
3257 ));
3258 }
3259
3260 #[test]
3261 fn scale_annotated_font_sizes_scales_span_shadow_geometry() {
3262 let _app_context = crate::render_state::app_context_test_scope();
3263 let text = crate::text::annotated_string::Builder::new()
3264 .push_style(crate::text::SpanStyle {
3265 shadow: Some(crate::text::Shadow {
3266 color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
3267 offset: crate::modifier::Point::new(6.0, 2.0),
3268 blur_radius: 4.0,
3269 }),
3270 ..Default::default()
3271 })
3272 .append("shadow")
3273 .pop()
3274 .to_annotated_string();
3275
3276 let scaled = scale_annotated_font_sizes(&text, FontScaleCurve::linear(0.5));
3277 let std::borrow::Cow::Owned(scaled) = scaled else {
3278 panic!("shadowed span should be scaled into owned text");
3279 };
3280 let shadow = scaled.span_styles[0]
3281 .item
3282 .shadow
3283 .expect("scaled span should retain shadow");
3284 assert_f32_close(shadow.offset.x, 3.0);
3285 assert_f32_close(shadow.offset.y, 1.0);
3286 assert_f32_close(shadow.blur_radius, 2.0);
3287 }
3288
3289 #[test]
3290 fn text_layout_options_does_not_wrap_on_tiny_width_delta() {
3291 let _app_context = crate::render_state::app_context_test_scope();
3292 let style = TextStyle {
3293 span_style: crate::text::SpanStyle {
3294 font_size: TextUnit::Sp(10.0),
3295 ..Default::default()
3296 },
3297 ..Default::default()
3298 };
3299 let options = TextLayoutOptions {
3300 overflow: TextOverflow::Clip,
3301 soft_wrap: true,
3302 max_lines: usize::MAX,
3303 min_lines: 1,
3304 };
3305
3306 let text = "if counter % 2 == 0";
3307 let exact_width = measure_text(&crate::text::AnnotatedString::from(text), &style).width;
3308 let prepared = prepare_text_layout(
3309 &crate::text::AnnotatedString::from(text),
3310 &style,
3311 options,
3312 Some(exact_width - 0.1),
3313 );
3314
3315 assert!(
3316 !prepared.text.text.contains('\n'),
3317 "unexpected line split: {:?}",
3318 prepared.text
3319 );
3320 }
3321
3322 #[test]
3323 fn line_break_mode_changes_wrap_strategy_contract() {
3324 let _app_context = crate::render_state::app_context_test_scope();
3325 let text = "This is an example text";
3326 let options = TextLayoutOptions {
3327 overflow: TextOverflow::Clip,
3328 soft_wrap: true,
3329 max_lines: usize::MAX,
3330 min_lines: 1,
3331 };
3332
3333 let simple = prepare_text_layout(
3334 &crate::text::AnnotatedString::from(text),
3335 &style_with_line_break(LineBreak::Simple),
3336 options,
3337 Some(120.0),
3338 );
3339 let heading = prepare_text_layout(
3340 &crate::text::AnnotatedString::from(text),
3341 &style_with_line_break(LineBreak::Heading),
3342 options,
3343 Some(120.0),
3344 );
3345 let paragraph = prepare_text_layout(
3346 &crate::text::AnnotatedString::from(text),
3347 &style_with_line_break(LineBreak::Paragraph),
3348 options,
3349 Some(50.0),
3350 );
3351
3352 assert_eq!(
3353 simple.text.text.lines().collect::<Vec<_>>(),
3354 vec!["This is an example", "text"]
3355 );
3356 assert_eq!(
3357 heading.text.text.lines().collect::<Vec<_>>(),
3358 vec!["This is an", "example text"]
3359 );
3360 assert_eq!(
3361 paragraph.text.text.lines().collect::<Vec<_>>(),
3362 vec!["This", "is an", "example", "text"]
3363 );
3364 }
3365
3366 #[test]
3367 fn hyphens_mode_changes_wrap_strategy_contract() {
3368 let _app_context = crate::render_state::app_context_test_scope();
3369 let text = "Transformation";
3370 let options = TextLayoutOptions {
3371 overflow: TextOverflow::Clip,
3372 soft_wrap: true,
3373 max_lines: usize::MAX,
3374 min_lines: 1,
3375 };
3376
3377 let auto = prepare_text_layout(
3378 &crate::text::AnnotatedString::from(text),
3379 &style_with_hyphens(Hyphens::Auto),
3380 options,
3381 Some(24.0),
3382 );
3383 let none = prepare_text_layout(
3384 &crate::text::AnnotatedString::from(text),
3385 &style_with_hyphens(Hyphens::None),
3386 options,
3387 Some(24.0),
3388 );
3389
3390 assert_eq!(
3391 auto.text.text.lines().collect::<Vec<_>>(),
3392 vec!["Tran", "sfor", "ma", "tion"]
3393 );
3394 assert_eq!(
3395 none.text.text.lines().collect::<Vec<_>>(),
3396 vec!["Tran", "sfor", "mati", "on"]
3397 );
3398 assert!(
3399 !auto.text.text.contains('-'),
3400 "automatic hyphenation should influence breaks without mutating source text content"
3401 );
3402 }
3403
3404 #[test]
3405 fn hyphens_auto_uses_measurer_hyphen_contract_when_valid() {
3406 let _app_context = crate::render_state::app_context_test_scope();
3407 let text = "Transformation";
3408 let style = style_with_hyphens(Hyphens::Auto);
3409 let options = TextLayoutOptions {
3410 overflow: TextOverflow::Clip,
3411 soft_wrap: true,
3412 max_lines: usize::MAX,
3413 min_lines: 1,
3414 };
3415
3416 let prepared = prepare_text_layout_fallback(
3417 &ContractBreakMeasurer { retreat: 1 },
3418 &crate::text::AnnotatedString::from(text),
3419 &style,
3420 options,
3421 Some(24.0),
3422 );
3423
3424 assert_eq!(
3425 prepared.text.text.lines().collect::<Vec<_>>(),
3426 vec!["Tra", "nsf", "orm", "ati", "on"]
3427 );
3428 }
3429
3430 #[test]
3431 fn hyphens_auto_falls_back_when_measurer_hyphen_contract_is_invalid() {
3432 let _app_context = crate::render_state::app_context_test_scope();
3433 let text = "Transformation";
3434 let style = style_with_hyphens(Hyphens::Auto);
3435 let options = TextLayoutOptions {
3436 overflow: TextOverflow::Clip,
3437 soft_wrap: true,
3438 max_lines: usize::MAX,
3439 min_lines: 1,
3440 };
3441
3442 let prepared = prepare_text_layout_fallback(
3443 &ContractBreakMeasurer { retreat: 10 },
3444 &crate::text::AnnotatedString::from(text),
3445 &style,
3446 options,
3447 Some(24.0),
3448 );
3449
3450 assert_eq!(
3451 prepared.text.text.lines().collect::<Vec<_>>(),
3452 vec!["Tran", "sfor", "ma", "tion"]
3453 );
3454 }
3455
3456 #[test]
3457 fn transformed_text_keeps_span_ranges_within_display_bounds() {
3458 let _app_context = crate::render_state::app_context_test_scope();
3459 let style = TextStyle {
3460 span_style: crate::text::SpanStyle {
3461 font_size: TextUnit::Sp(10.0),
3462 ..Default::default()
3463 },
3464 ..Default::default()
3465 };
3466 let options = TextLayoutOptions {
3467 overflow: TextOverflow::Ellipsis,
3468 soft_wrap: false,
3469 max_lines: 1,
3470 min_lines: 1,
3471 };
3472 let annotated = crate::text::AnnotatedString::builder()
3473 .push_style(crate::text::SpanStyle {
3474 font_weight: Some(crate::text::FontWeight::BOLD),
3475 ..Default::default()
3476 })
3477 .append("Styled overflow text sample")
3478 .pop()
3479 .to_annotated_string();
3480
3481 let prepared = prepare_text_layout(&annotated, &style, options, Some(40.0));
3482 assert!(prepared.did_overflow);
3483 for span in &prepared.text.span_styles {
3484 assert!(span.range.start < span.range.end);
3485 assert!(span.range.end <= prepared.text.text.len());
3486 assert!(prepared.text.text.is_char_boundary(span.range.start));
3487 assert!(prepared.text.text.is_char_boundary(span.range.end));
3488 }
3489 }
3490
3491 #[test]
3492 fn a_word_that_fits_stays_on_the_line_when_the_space_after_it_fits_too() {
3493 let _app_context = crate::render_state::app_context_test_scope();
3494 let style = TextStyle {
3495 span_style: crate::text::SpanStyle {
3496 font_size: TextUnit::Sp(10.0),
3497 ..Default::default()
3498 },
3499 ..Default::default()
3500 };
3501 let options = TextLayoutOptions {
3502 overflow: TextOverflow::Clip,
3503 soft_wrap: true,
3504 max_lines: usize::MAX,
3505 min_lines: 1,
3506 };
3507 let width_of = |text: &str| {
3508 measure_text_with_options(
3509 &crate::text::AnnotatedString::from(text.to_string()),
3510 &style,
3511 options,
3512 None,
3513 )
3514 .width
3515 };
3516 let fits = width_of("aa bb ");
3517 let overflows = width_of("aa bb c");
3518 assert!(overflows > fits, "the fixture needs a real gap here");
3519 let max_width = (fits + overflows) * 0.5;
3520
3521 let prepared = prepare_text_layout(
3522 &crate::text::AnnotatedString::from("aa bb cc".to_string()),
3523 &style,
3524 options,
3525 Some(max_width),
3526 );
3527 assert_eq!(prepared.text.text, "aa bb\ncc");
3528 }
3529
3530 #[test]
3531 fn wrapped_text_splits_styles_around_inserted_newlines() {
3532 let _app_context = crate::render_state::app_context_test_scope();
3533 let style = TextStyle {
3534 span_style: crate::text::SpanStyle {
3535 font_size: TextUnit::Sp(10.0),
3536 ..Default::default()
3537 },
3538 ..Default::default()
3539 };
3540 let options = TextLayoutOptions {
3541 overflow: TextOverflow::Clip,
3542 soft_wrap: true,
3543 max_lines: usize::MAX,
3544 min_lines: 1,
3545 };
3546 let annotated = crate::text::AnnotatedString::builder()
3547 .push_style(crate::text::SpanStyle {
3548 text_decoration: Some(crate::text::TextDecoration::UNDERLINE),
3549 ..Default::default()
3550 })
3551 .append("Wrapped style text example")
3552 .pop()
3553 .to_annotated_string();
3554
3555 let prepared = prepare_text_layout(&annotated, &style, options, Some(32.0));
3556 assert!(prepared.text.text.contains('\n'));
3557 assert!(!prepared.text.span_styles.is_empty());
3558 for span in &prepared.text.span_styles {
3559 assert!(span.range.end <= prepared.text.text.len());
3560 }
3561 }
3562
3563 #[test]
3564 fn mixed_font_size_segments_wrap_without_truncation() {
3565 let _app_context = crate::render_state::app_context_test_scope();
3566 let style = TextStyle {
3567 span_style: crate::text::SpanStyle {
3568 font_size: TextUnit::Sp(14.0),
3569 ..Default::default()
3570 },
3571 ..Default::default()
3572 };
3573 let options = TextLayoutOptions {
3574 overflow: TextOverflow::Clip,
3575 soft_wrap: true,
3576 max_lines: usize::MAX,
3577 min_lines: 1,
3578 };
3579 let annotated = crate::text::AnnotatedString::builder()
3580 .append("You can also ")
3581 .push_style(crate::text::SpanStyle {
3582 font_size: TextUnit::Sp(22.0),
3583 ..Default::default()
3584 })
3585 .append("change font size")
3586 .pop()
3587 .append(" dynamically mid-sentence!")
3588 .to_annotated_string();
3589
3590 let prepared = prepare_text_layout(&annotated, &style, options, Some(260.0));
3591 assert!(prepared.text.text.contains('\n'));
3592 assert!(prepared.text.text.contains("mid-sentence!"));
3593 assert!(!prepared.did_overflow);
3594 }
3595}