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