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