1use std::{
2 borrow::Cow,
3 cell::{Cell, RefCell},
4 collections::{HashMap, VecDeque, hash_map::Entry},
5 hash::Hash,
6 ops::Range,
7 rc::Rc,
8};
9
10use cranpose_core::NodeId;
11use web_time::Instant;
12
13use super::{
14 layout_options::{TextLayoutOptions, TextOverflow},
15 paragraph::{Hyphens, LineBreak},
16 style::TextStyle,
17};
18use crate::{font_scale::FontScaleCurve, text_layout_result::TextLayoutResult};
19
20const ELLIPSIS: &str = "\u{2026}";
21const DEFAULT_FONT_SIZE_SP: f32 = 14.0;
22const WRAP_EPSILON: f32 = 0.5;
23const SCALE_DOWN_SEARCH_STEPS: usize = 14;
24const AUTO_HYPHEN_MIN_SEGMENT_CHARS: usize = 2;
25const AUTO_HYPHEN_MIN_TRAILING_CHARS: usize = 3;
26const AUTO_HYPHEN_PREFERRED_TRAILING_CHARS: usize = 4;
27const TEXT_SERVICE_CACHE_CAPACITY: usize = 8192;
28const TEXT_LAYOUT_TELEMETRY_ENV: &str = "CRANPOSE_TEXT_LAYOUT_TELEMETRY";
29
30fn text_layout_telemetry_enabled() -> bool {
31 cranpose_core::env_flag!(TEXT_LAYOUT_TELEMETRY_ENV)
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct TextMetrics {
36 pub width: f32,
37 pub height: f32,
38 pub line_height: f32,
40 pub line_count: usize,
42}
43
44#[derive(Clone, Debug, PartialEq)]
45pub struct PreparedTextLayout {
46 pub text: crate::text::AnnotatedString,
47 pub visual_style: TextStyle,
48 pub metrics: TextMetrics,
49 pub did_overflow: bool,
50}
51
52#[derive(Clone, Debug, PartialEq)]
53pub struct TextLinePrefixWidths {
54 prefix_widths: Vec<f32>,
55 separator_before: Vec<f32>,
56 non_empty_overhang: f32,
57}
58
59impl TextLinePrefixWidths {
60 pub fn from_parts(
61 prefix_widths: Vec<f32>,
62 separator_before: Vec<f32>,
63 non_empty_overhang: f32,
64 ) -> Option<Self> {
65 if prefix_widths.is_empty() || prefix_widths.len() != separator_before.len() + 1 {
66 return None;
67 }
68 if prefix_widths
69 .iter()
70 .chain(separator_before.iter())
71 .any(|value| !value.is_finite())
72 {
73 return None;
74 }
75 let non_empty_overhang = non_empty_overhang.max(0.0);
76 if !non_empty_overhang.is_finite() {
77 return None;
78 }
79 Some(Self {
80 prefix_widths,
81 separator_before,
82 non_empty_overhang,
83 })
84 }
85
86 pub fn monospaced(char_count: usize, char_width: f32, letter_spacing: f32) -> Option<Self> {
87 if !char_width.is_finite() || !letter_spacing.is_finite() {
88 return None;
89 }
90 let char_width = char_width.max(0.0);
91 let letter_spacing = letter_spacing.max(0.0);
92 let mut prefix_widths = Vec::with_capacity(char_count + 1);
93 let mut separator_before = Vec::with_capacity(char_count);
94 let mut width = 0.0f32;
95 prefix_widths.push(width);
96 for _ in 0..char_count {
97 separator_before.push(0.0);
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 && start < range_end
1617 {
1618 remapped.push(crate::text::RangeStyle {
1619 item: style.item.clone(),
1620 range: start..range_end,
1621 });
1622 }
1623 }
1624
1625 if let Some(start) = range_start.take()
1626 && start < range_end
1627 {
1628 remapped.push(crate::text::RangeStyle {
1629 item: style.item.clone(),
1630 range: start..range_end,
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 && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1702 {
1703 return Some(width);
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 && measured_width <= max_width + WRAP_EPSILON
1739 {
1740 return vec![DisplayLine::from_measured_source_range(
1741 line_range,
1742 measured_width,
1743 )];
1744 }
1745
1746 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1747 && line_text.chars().any(char::is_whitespace)
1748 && let Some(balanced) = wrap_line_with_word_balance(
1749 measurer,
1750 text,
1751 line_range.clone(),
1752 style,
1753 max_width,
1754 line_break,
1755 )
1756 {
1757 return balanced;
1758 }
1759
1760 wrap_line_greedy(
1761 measurer, text, line_range, style, max_width, line_break, hyphens,
1762 )
1763}
1764
1765fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1766 measurer: &M,
1767 text: &crate::text::AnnotatedString,
1768 line_range: Range<usize>,
1769 style: &TextStyle,
1770 max_width: f32,
1771 line_break: LineBreak,
1772 hyphens: Hyphens,
1773) -> Vec<DisplayLine> {
1774 let line_text = &text.text[line_range.clone()];
1775 let boundaries = char_boundaries(line_text);
1776 let measure_context =
1777 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1778 if let Some(measured_width) =
1779 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1780 && measured_width <= max_width + WRAP_EPSILON
1781 {
1782 return vec![DisplayLine::from_measured_source_range(
1783 line_range,
1784 measured_width,
1785 )];
1786 }
1787 let mut wrapped = Vec::new();
1788 let mut start_idx = 0usize;
1789
1790 while start_idx < boundaries.len() - 1 {
1791 let mut low = start_idx + 1;
1792 let mut high = boundaries.len() - 1;
1793 let mut best = start_idx + 1;
1794
1795 while low <= high {
1796 let mid = (low + high) / 2;
1797 let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1798 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1799 best = mid;
1800 low = mid + 1;
1801 } else {
1802 if mid == 0 {
1803 break;
1804 }
1805 high = mid - 1;
1806 }
1807 }
1808
1809 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1810 let mut effective_wrap_idx = wrap_idx;
1811 let can_hyphenate = hyphens == Hyphens::Auto
1812 && wrap_idx == best
1813 && best < boundaries.len() - 1
1814 && is_break_inside_word(line_text, &boundaries, wrap_idx);
1815 if can_hyphenate {
1816 effective_wrap_idx = resolve_auto_hyphen_break(
1817 measurer,
1818 line_text,
1819 style,
1820 &boundaries,
1821 start_idx,
1822 wrap_idx,
1823 );
1824 }
1825
1826 let broke_at_word_boundary = effective_wrap_idx > start_idx
1831 && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
1832 .chars()
1833 .all(char::is_whitespace);
1834 let segment_start = boundaries[start_idx];
1835 let mut segment_end = boundaries[effective_wrap_idx];
1836 if wrap_idx != best || broke_at_word_boundary {
1837 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1838 }
1839 let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1840 wrapped.push(measure_context.display_line_for_char_range(
1841 &boundaries,
1842 start_idx,
1843 segment_end_idx,
1844 ));
1845
1846 start_idx = if wrap_idx != best || broke_at_word_boundary {
1847 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1848 } else {
1849 effective_wrap_idx
1850 };
1851 }
1852
1853 if wrapped.is_empty() {
1854 wrapped.push(DisplayLine::from_source_range(
1855 line_range.start..line_range.start,
1856 ));
1857 }
1858
1859 wrapped
1860}
1861
1862fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1863 measurer: &M,
1864 text: &crate::text::AnnotatedString,
1865 line_range: Range<usize>,
1866 style: &TextStyle,
1867 max_width: f32,
1868 line_break: LineBreak,
1869) -> Option<Vec<DisplayLine>> {
1870 let line_text = &text.text[line_range.clone()];
1871 let boundaries = char_boundaries(line_text);
1872 let measure_context =
1873 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1874 if let Some(measured_width) =
1875 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1876 && measured_width <= max_width + WRAP_EPSILON
1877 {
1878 return Some(vec![DisplayLine::from_measured_source_range(
1879 line_range,
1880 measured_width,
1881 )]);
1882 }
1883 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1884 if breakpoints.len() <= 2 {
1885 return None;
1886 }
1887
1888 let node_count = breakpoints.len();
1889 let mut best_cost = vec![f32::INFINITY; node_count];
1890 let mut next_index = vec![None; node_count];
1891 best_cost[node_count - 1] = 0.0;
1892
1893 for start in (0..node_count - 1).rev() {
1894 for end in start + 1..node_count {
1895 let start_byte = boundaries[breakpoints[start]];
1896 let end_byte = boundaries[breakpoints[end]];
1897 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1898 if trimmed_end <= start_byte {
1899 continue;
1900 }
1901 let segment_start_idx = breakpoints[start];
1902 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1903 let segment_width =
1904 measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1905 if segment_width > max_width + WRAP_EPSILON {
1906 continue;
1907 }
1908 if !best_cost[end].is_finite() {
1909 continue;
1910 }
1911 let slack = (max_width - segment_width).max(0.0);
1912 let is_last = end == node_count - 1;
1913 let segment_cost = match line_break {
1914 LineBreak::Heading => slack * slack,
1915 LineBreak::Paragraph => {
1916 if is_last {
1917 slack * slack * 0.16
1918 } else {
1919 slack * slack
1920 }
1921 }
1922 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1923 };
1924 let candidate = segment_cost + best_cost[end];
1925 if candidate < best_cost[start] {
1926 best_cost[start] = candidate;
1927 next_index[start] = Some(end);
1928 }
1929 }
1930 }
1931
1932 let mut wrapped = Vec::new();
1933 let mut current = 0usize;
1934 while current < node_count - 1 {
1935 let next = next_index[current]?;
1936 let start_byte = boundaries[breakpoints[current]];
1937 let end_byte = boundaries[breakpoints[next]];
1938 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1939 if trimmed_end <= start_byte {
1940 return None;
1941 }
1942 let segment_start_idx = breakpoints[current];
1943 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1944 wrapped.push(measure_context.display_line_for_char_range(
1945 &boundaries,
1946 segment_start_idx,
1947 segment_end_idx,
1948 ));
1949 current = next;
1950 }
1951
1952 if wrapped.is_empty() {
1953 return None;
1954 }
1955
1956 Some(wrapped)
1957}
1958
1959fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1960 let mut points = vec![0usize];
1961 for idx in 1..boundaries.len() - 1 {
1962 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1963 let current = &line[boundaries[idx]..boundaries[idx + 1]];
1964 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1965 points.push(idx);
1966 }
1967 }
1968 let end = boundaries.len() - 1;
1969 if points.last().copied() != Some(end) {
1970 points.push(end);
1971 }
1972 points
1973}
1974
1975fn choose_wrap_break(
1976 line: &str,
1977 boundaries: &[usize],
1978 start_idx: usize,
1979 best: usize,
1980 _line_break: LineBreak,
1981) -> usize {
1982 if best >= boundaries.len() - 1 {
1983 return best;
1984 }
1985
1986 if best <= start_idx + 1 {
1987 return best;
1988 }
1989
1990 for idx in (start_idx + 1..=best).rev() {
1998 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1999 if prev.chars().all(char::is_whitespace) {
2000 return idx;
2001 }
2002 }
2003 best
2004}
2005
2006fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
2007 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
2008 return false;
2009 }
2010 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
2011 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
2012 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
2013}
2014
2015fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
2016 measurer: &M,
2017 line: &str,
2018 style: &TextStyle,
2019 boundaries: &[usize],
2020 start_idx: usize,
2021 break_idx: usize,
2022) -> usize {
2023 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
2024 && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
2025 {
2026 return candidate;
2027 }
2028 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
2029}
2030
2031fn is_valid_auto_hyphen_break(
2032 line: &str,
2033 boundaries: &[usize],
2034 start_idx: usize,
2035 break_idx: usize,
2036 candidate_idx: usize,
2037) -> bool {
2038 let end_idx = boundaries.len().saturating_sub(1);
2039 candidate_idx > start_idx
2040 && candidate_idx < end_idx
2041 && candidate_idx <= break_idx
2042 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
2043 && is_break_inside_word(line, boundaries, candidate_idx)
2044}
2045
2046fn choose_auto_hyphen_break_fallback(
2047 boundaries: &[usize],
2048 start_idx: usize,
2049 break_idx: usize,
2050) -> usize {
2051 let end_idx = boundaries.len().saturating_sub(1);
2052 if break_idx >= end_idx {
2053 return break_idx;
2054 }
2055 let trailing_len = end_idx.saturating_sub(break_idx);
2056 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
2057 return break_idx;
2058 }
2059
2060 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
2061 let max_break = break_idx.saturating_sub(1);
2062 if min_break > max_break {
2063 return break_idx;
2064 }
2065
2066 let mut best_break = break_idx;
2067 let mut best_penalty = usize::MAX;
2068 for idx in min_break..=max_break {
2069 let candidate_trailing_len = end_idx.saturating_sub(idx);
2070 let candidate_prefix_len = idx.saturating_sub(start_idx);
2071 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
2072 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
2073 {
2074 continue;
2075 }
2076
2077 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
2078 if penalty < best_penalty {
2079 best_penalty = penalty;
2080 best_break = idx;
2081 if penalty == 0 {
2082 break;
2083 }
2084 }
2085 }
2086 best_break
2087}
2088
2089fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
2090 while idx < boundaries.len() - 1 {
2091 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
2092 if !ch.chars().all(char::is_whitespace) {
2093 break;
2094 }
2095 idx += 1;
2096 }
2097 idx
2098}
2099
2100fn apply_line_overflow<M: TextMeasurer + ?Sized>(
2101 measurer: &M,
2102 line: &str,
2103 style: &TextStyle,
2104 max_width: Option<f32>,
2105 options: TextLayoutOptions,
2106 is_last_visible_line: bool,
2107 single_line_ellipsis: bool,
2108) -> String {
2109 if options.overflow == TextOverflow::Clip || !is_last_visible_line {
2110 return line.to_string();
2111 }
2112
2113 let Some(width_limit) = max_width else {
2114 return match options.overflow {
2115 TextOverflow::Ellipsis => format!("{line}{ELLIPSIS}"),
2116 TextOverflow::StartEllipsis => format!("{ELLIPSIS}{line}"),
2117 TextOverflow::MiddleEllipsis => format!("{ELLIPSIS}{line}"),
2118 TextOverflow::Clip | TextOverflow::Visible | TextOverflow::ScaleDown { .. } => {
2119 line.to_string()
2120 }
2121 };
2122 };
2123
2124 match options.overflow {
2125 TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
2126 TextOverflow::Ellipsis => fit_end_ellipsis(measurer, line, style, width_limit),
2127 TextOverflow::StartEllipsis => {
2128 if single_line_ellipsis {
2129 fit_start_ellipsis(measurer, line, style, width_limit)
2130 } else {
2131 line.to_string()
2132 }
2133 }
2134 TextOverflow::MiddleEllipsis => {
2135 if single_line_ellipsis {
2136 fit_middle_ellipsis(measurer, line, style, width_limit)
2137 } else {
2138 line.to_string()
2139 }
2140 }
2141 TextOverflow::ScaleDown { .. } => line.to_string(),
2142 }
2143}
2144
2145fn fit_end_ellipsis<M: TextMeasurer + ?Sized>(
2146 measurer: &M,
2147 line: &str,
2148 style: &TextStyle,
2149 max_width: f32,
2150) -> String {
2151 if measurer
2152 .measure(&crate::text::AnnotatedString::from(line), style)
2153 .width
2154 <= max_width + WRAP_EPSILON
2155 {
2156 return line.to_string();
2157 }
2158
2159 let ellipsis_width = measurer
2160 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
2161 .width;
2162 if ellipsis_width > max_width + WRAP_EPSILON {
2163 return String::new();
2164 }
2165
2166 let boundaries = char_boundaries(line);
2167 let mut low = 0usize;
2168 let mut high = boundaries.len() - 1;
2169 let mut best = 0usize;
2170
2171 while low <= high {
2172 let mid = (low + high) / 2;
2173 let prefix = &line[..boundaries[mid]];
2174 let candidate = format!("{prefix}{ELLIPSIS}");
2175 let width = measurer
2176 .measure(
2177 &crate::text::AnnotatedString::from(candidate.as_str()),
2178 style,
2179 )
2180 .width;
2181 if width <= max_width + WRAP_EPSILON {
2182 best = mid;
2183 low = mid + 1;
2184 } else if mid == 0 {
2185 break;
2186 } else {
2187 high = mid - 1;
2188 }
2189 }
2190
2191 format!("{}{}", &line[..boundaries[best]], ELLIPSIS)
2192}
2193
2194fn fit_start_ellipsis<M: TextMeasurer + ?Sized>(
2195 measurer: &M,
2196 line: &str,
2197 style: &TextStyle,
2198 max_width: f32,
2199) -> String {
2200 if measurer
2201 .measure(&crate::text::AnnotatedString::from(line), style)
2202 .width
2203 <= max_width + WRAP_EPSILON
2204 {
2205 return line.to_string();
2206 }
2207
2208 let ellipsis_width = measurer
2209 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
2210 .width;
2211 if ellipsis_width > max_width + WRAP_EPSILON {
2212 return String::new();
2213 }
2214
2215 let boundaries = char_boundaries(line);
2216 let mut low = 0usize;
2217 let mut high = boundaries.len() - 1;
2218 let mut best = boundaries.len() - 1;
2219
2220 while low <= high {
2221 let mid = (low + high) / 2;
2222 let suffix = &line[boundaries[mid]..];
2223 let candidate = format!("{ELLIPSIS}{suffix}");
2224 let width = measurer
2225 .measure(
2226 &crate::text::AnnotatedString::from(candidate.as_str()),
2227 style,
2228 )
2229 .width;
2230 if width <= max_width + WRAP_EPSILON {
2231 best = mid;
2232 if mid == 0 {
2233 break;
2234 }
2235 high = mid - 1;
2236 } else {
2237 low = mid + 1;
2238 }
2239 }
2240
2241 format!("{ELLIPSIS}{}", &line[boundaries[best]..])
2242}
2243
2244fn fit_middle_ellipsis<M: TextMeasurer + ?Sized>(
2245 measurer: &M,
2246 line: &str,
2247 style: &TextStyle,
2248 max_width: f32,
2249) -> String {
2250 if measurer
2251 .measure(&crate::text::AnnotatedString::from(line), style)
2252 .width
2253 <= max_width + WRAP_EPSILON
2254 {
2255 return line.to_string();
2256 }
2257
2258 let ellipsis_width = measurer
2259 .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
2260 .width;
2261 if ellipsis_width > max_width + WRAP_EPSILON {
2262 return String::new();
2263 }
2264
2265 let boundaries = char_boundaries(line);
2266 let total_chars = boundaries.len().saturating_sub(1);
2267 for keep in (0..=total_chars).rev() {
2268 let keep_start = keep.div_ceil(2);
2269 let keep_end = keep / 2;
2270 let start = &line[..boundaries[keep_start]];
2271 let end_start = boundaries[total_chars.saturating_sub(keep_end)];
2272 let end = &line[end_start..];
2273 let candidate = format!("{start}{ELLIPSIS}{end}");
2274 if measurer
2275 .measure(
2276 &crate::text::AnnotatedString::from(candidate.as_str()),
2277 style,
2278 )
2279 .width
2280 <= max_width + WRAP_EPSILON
2281 {
2282 return candidate;
2283 }
2284 }
2285
2286 ELLIPSIS.to_string()
2287}
2288
2289fn char_boundaries(text: &str) -> Vec<usize> {
2290 let mut out = Vec::with_capacity(text.chars().count() + 1);
2291 out.push(0);
2292 for (idx, _) in text.char_indices() {
2293 if idx != 0 {
2294 out.push(idx);
2295 }
2296 }
2297 out.push(text.len());
2298 out
2299}
2300
2301#[cfg(test)]
2302mod tests {
2303 use std::cell::Cell;
2304
2305 use super::*;
2306 use crate::{
2307 text::{Hyphens, LineBreak, ParagraphStyle, TextUnit},
2308 text_layout_result::TextLayoutResult,
2309 };
2310
2311 #[test]
2312 fn text_layout_telemetry_env_flag_is_not_process_cached() {
2313 let source = include_str!("measure.rs");
2314 let once_lock = ["Once", "Lock"].concat();
2315 let cached_init_call = ["get", "_or", "_init"].concat();
2316
2317 assert!(
2318 !source.contains(&once_lock) && !source.contains(&cached_init_call),
2319 "text layout telemetry env flag must be read at the diagnostic boundary"
2320 );
2321 }
2322
2323 #[test]
2324 fn prepared_layout_cache_distinguishes_visual_styles() {
2325 let service = TextService::new();
2326 let text = crate::text::AnnotatedString::from("tinted".to_string());
2327 let options = TextLayoutOptions::default();
2328
2329 let mut style = TextStyle::default();
2330 style.span_style.color = Some(crate::Color(1.0, 0.0, 0.0, 1.0));
2331 let red = service.prepare_with_options(None, &text, &style, options, None);
2332
2333 style.span_style.color = Some(crate::Color(0.0, 0.0, 1.0, 1.0));
2334 let blue = service.prepare_with_options(None, &text, &style, options, None);
2335
2336 assert_eq!(
2337 red.visual_style.span_style.color,
2338 Some(crate::Color(1.0, 0.0, 0.0, 1.0)),
2339 );
2340 assert_eq!(
2341 blue.visual_style.span_style.color,
2342 Some(crate::Color(0.0, 0.0, 1.0, 1.0)),
2343 "a color-only style change must not be served a stale prepared layout \
2344 (measurement hashes ignore visual attributes by design)"
2345 );
2346 }
2347
2348 #[test]
2349 fn system_font_scale_changes_sp_measurement_and_prepared_text() {
2350 let _app_context = crate::render_state::app_context_test_scope();
2351 let text = crate::text::AnnotatedString::from("scale me");
2352 let style = TextStyle {
2353 span_style: crate::text::SpanStyle {
2354 font_size: TextUnit::Sp(10.0),
2355 ..Default::default()
2356 },
2357 ..Default::default()
2358 };
2359
2360 let unscaled = measure_text(&text, &style);
2361 crate::set_font_scale(2.0);
2362 let scaled = measure_text(&text, &style);
2363 let prepared = prepare_text_layout(&text, &style, TextLayoutOptions::default(), None);
2364
2365 assert!((scaled.width - unscaled.width * 2.0).abs() <= f32::EPSILON);
2366 assert!((scaled.height - unscaled.height * 2.0).abs() <= f32::EPSILON);
2367 assert_eq!(
2368 prepared.visual_style.span_style.font_size,
2369 TextUnit::Sp(20.0)
2370 );
2371 }
2372
2373 #[test]
2374 fn a_platform_curve_resolves_an_sp_where_the_platform_does_and_not_where_a_multiplier_would() {
2375 let _app_context = crate::render_state::app_context_test_scope();
2384 let text = crate::text::AnnotatedString::from("SOLID next at 3 gold");
2385 let style = TextStyle {
2386 span_style: crate::text::SpanStyle {
2387 font_size: TextUnit::Sp(13.0),
2388 letter_spacing: TextUnit::Sp(0.4),
2389 ..Default::default()
2390 },
2391 ..Default::default()
2392 };
2393
2394 crate::set_font_scale_curve(FontScaleCurve::from_samples(
2395 1.24,
2396 &[
2397 (8.0, 9.92),
2398 (10.0, 12.4),
2399 (12.0, 14.88),
2400 (14.0, 17.84),
2401 (16.0, 19.36),
2402 (18.0, 20.88),
2403 (20.0, 22.88),
2404 (24.0, 25.92),
2405 (30.0, 30.0),
2406 (100.0, 100.0),
2407 ],
2408 ));
2409 let prepared = prepare_text_layout(&text, &style, TextLayoutOptions::default(), None);
2410 assert_eq!(
2411 prepared.visual_style.span_style.font_size,
2412 TextUnit::Sp(16.36)
2413 );
2414 assert_eq!(
2417 prepared.visual_style.span_style.letter_spacing,
2418 TextUnit::Sp(0.4 * 1.24)
2419 );
2420 assert_eq!(crate::current_font_scale(), 1.24);
2423
2424 crate::set_font_scale(1.24);
2425 let multiplied = prepare_text_layout(&text, &style, TextLayoutOptions::default(), None);
2426 assert_eq!(
2427 multiplied.visual_style.span_style.font_size,
2428 TextUnit::Sp(13.0 * 1.24)
2429 );
2430 }
2431
2432 #[test]
2433 fn text_service_cache_retains_large_lazy_text_working_set() {
2434 let mut cache = BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY);
2435 let metrics = TextMetrics {
2436 width: 1.0,
2437 height: 1.0,
2438 line_height: 1.0,
2439 line_count: 1,
2440 };
2441
2442 for index in 0..4096u64 {
2443 cache.insert(
2444 TextBaseCacheKey {
2445 text_hash: index,
2446 style_hash: 7,
2447 },
2448 metrics,
2449 );
2450 }
2451
2452 for index in 0..4096u64 {
2453 assert!(
2454 cache
2455 .get(&TextBaseCacheKey {
2456 text_hash: index,
2457 style_hash: 7,
2458 })
2459 .is_some(),
2460 "large lazy text working-set entry {index} was evicted too early"
2461 );
2462 }
2463 }
2464
2465 struct ContractBreakMeasurer {
2466 retreat: usize,
2467 }
2468
2469 impl TextMeasurer for ContractBreakMeasurer {
2470 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2471 MonospacedTextMeasurer.measure(
2472 &crate::text::AnnotatedString::from(text.text.as_str()),
2473 style,
2474 )
2475 }
2476
2477 fn get_offset_for_position(
2478 &self,
2479 text: &crate::text::AnnotatedString,
2480 style: &TextStyle,
2481 x: f32,
2482 y: f32,
2483 ) -> usize {
2484 MonospacedTextMeasurer.get_offset_for_position(
2485 &crate::text::AnnotatedString::from(text.text.as_str()),
2486 style,
2487 x,
2488 y,
2489 )
2490 }
2491
2492 fn get_cursor_x_for_offset(
2493 &self,
2494 text: &crate::text::AnnotatedString,
2495 style: &TextStyle,
2496 offset: usize,
2497 ) -> f32 {
2498 MonospacedTextMeasurer.get_cursor_x_for_offset(
2499 &crate::text::AnnotatedString::from(text.text.as_str()),
2500 style,
2501 offset,
2502 )
2503 }
2504
2505 fn layout(
2506 &self,
2507 text: &crate::text::AnnotatedString,
2508 style: &TextStyle,
2509 ) -> TextLayoutResult {
2510 MonospacedTextMeasurer.layout(
2511 &crate::text::AnnotatedString::from(text.text.as_str()),
2512 style,
2513 )
2514 }
2515
2516 fn choose_auto_hyphen_break(
2517 &self,
2518 _line: &str,
2519 _style: &TextStyle,
2520 _segment_start_char: usize,
2521 measured_break_char: usize,
2522 ) -> Option<usize> {
2523 measured_break_char.checked_sub(self.retreat)
2524 }
2525 }
2526
2527 struct CountingTextMeasurer {
2528 measure_calls: Rc<Cell<usize>>,
2529 layout_calls: Rc<Cell<usize>>,
2530 }
2531
2532 impl CountingTextMeasurer {
2533 fn new(measure_calls: Rc<Cell<usize>>, layout_calls: Rc<Cell<usize>>) -> Self {
2534 Self {
2535 measure_calls,
2536 layout_calls,
2537 }
2538 }
2539 }
2540
2541 impl TextMeasurer for CountingTextMeasurer {
2542 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2543 self.measure_calls.set(self.measure_calls.get() + 1);
2544 MonospacedTextMeasurer.measure(text, style)
2545 }
2546
2547 fn get_offset_for_position(
2548 &self,
2549 text: &crate::text::AnnotatedString,
2550 style: &TextStyle,
2551 x: f32,
2552 y: f32,
2553 ) -> usize {
2554 MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2555 }
2556
2557 fn get_cursor_x_for_offset(
2558 &self,
2559 text: &crate::text::AnnotatedString,
2560 style: &TextStyle,
2561 offset: usize,
2562 ) -> f32 {
2563 MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2564 }
2565
2566 fn layout(
2567 &self,
2568 text: &crate::text::AnnotatedString,
2569 style: &TextStyle,
2570 ) -> TextLayoutResult {
2571 self.layout_calls.set(self.layout_calls.get() + 1);
2572 MonospacedTextMeasurer.layout(text, style)
2573 }
2574 }
2575
2576 struct CountingPreparedTextMeasurer {
2577 prepare_calls: Rc<Cell<usize>>,
2578 }
2579
2580 impl CountingPreparedTextMeasurer {
2581 fn new(prepare_calls: Rc<Cell<usize>>) -> Self {
2582 Self { prepare_calls }
2583 }
2584 }
2585
2586 struct PrefixWidthCountingMeasurer {
2587 prefix_calls: Rc<Cell<usize>>,
2588 subsequence_calls: Rc<Cell<usize>>,
2589 }
2590
2591 impl PrefixWidthCountingMeasurer {
2592 fn new(prefix_calls: Rc<Cell<usize>>, subsequence_calls: Rc<Cell<usize>>) -> Self {
2593 Self {
2594 prefix_calls,
2595 subsequence_calls,
2596 }
2597 }
2598 }
2599
2600 impl TextMeasurer for PrefixWidthCountingMeasurer {
2601 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2602 MonospacedTextMeasurer.measure(text, style)
2603 }
2604
2605 fn measure_subsequence(
2606 &self,
2607 text: &crate::text::AnnotatedString,
2608 range: Range<usize>,
2609 style: &TextStyle,
2610 ) -> TextMetrics {
2611 self.subsequence_calls.set(self.subsequence_calls.get() + 1);
2612 MonospacedTextMeasurer.measure_subsequence(text, range, style)
2613 }
2614
2615 fn measure_line_prefix_widths(
2616 &self,
2617 text: &crate::text::AnnotatedString,
2618 line_range: Range<usize>,
2619 style: &TextStyle,
2620 ) -> Option<TextLinePrefixWidths> {
2621 self.prefix_calls.set(self.prefix_calls.get() + 1);
2622 MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2623 }
2624
2625 fn get_offset_for_position(
2626 &self,
2627 text: &crate::text::AnnotatedString,
2628 style: &TextStyle,
2629 x: f32,
2630 y: f32,
2631 ) -> usize {
2632 MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2633 }
2634
2635 fn get_cursor_x_for_offset(
2636 &self,
2637 text: &crate::text::AnnotatedString,
2638 style: &TextStyle,
2639 offset: usize,
2640 ) -> f32 {
2641 MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2642 }
2643
2644 fn layout(
2645 &self,
2646 text: &crate::text::AnnotatedString,
2647 style: &TextStyle,
2648 ) -> TextLayoutResult {
2649 MonospacedTextMeasurer.layout(text, style)
2650 }
2651 }
2652
2653 struct LineHeightCountingMeasurer {
2654 measure_calls: Rc<Cell<usize>>,
2655 line_height_calls: Rc<Cell<usize>>,
2656 }
2657
2658 struct FitProbeCountingMeasurer {
2659 line_width_calls: Rc<Cell<usize>>,
2660 prefix_calls: Rc<Cell<usize>>,
2661 }
2662
2663 impl FitProbeCountingMeasurer {
2664 fn new(line_width_calls: Rc<Cell<usize>>, prefix_calls: Rc<Cell<usize>>) -> Self {
2665 Self {
2666 line_width_calls,
2667 prefix_calls,
2668 }
2669 }
2670 }
2671
2672 impl LineHeightCountingMeasurer {
2673 fn new(measure_calls: Rc<Cell<usize>>, line_height_calls: Rc<Cell<usize>>) -> Self {
2674 Self {
2675 measure_calls,
2676 line_height_calls,
2677 }
2678 }
2679 }
2680
2681 impl TextMeasurer for LineHeightCountingMeasurer {
2682 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2683 self.measure_calls.set(self.measure_calls.get() + 1);
2684 MonospacedTextMeasurer.measure(text, style)
2685 }
2686
2687 fn measure_line_prefix_widths(
2688 &self,
2689 text: &crate::text::AnnotatedString,
2690 line_range: Range<usize>,
2691 style: &TextStyle,
2692 ) -> Option<TextLinePrefixWidths> {
2693 MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2694 }
2695
2696 fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
2697 self.line_height_calls.set(self.line_height_calls.get() + 1);
2698 MonospacedTextMeasurer.line_height(text, style)
2699 }
2700
2701 fn get_offset_for_position(
2702 &self,
2703 text: &crate::text::AnnotatedString,
2704 style: &TextStyle,
2705 x: f32,
2706 y: f32,
2707 ) -> usize {
2708 MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2709 }
2710
2711 fn get_cursor_x_for_offset(
2712 &self,
2713 text: &crate::text::AnnotatedString,
2714 style: &TextStyle,
2715 offset: usize,
2716 ) -> f32 {
2717 MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2718 }
2719
2720 fn layout(
2721 &self,
2722 text: &crate::text::AnnotatedString,
2723 style: &TextStyle,
2724 ) -> TextLayoutResult {
2725 MonospacedTextMeasurer.layout(text, style)
2726 }
2727 }
2728
2729 impl TextMeasurer for FitProbeCountingMeasurer {
2730 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2731 MonospacedTextMeasurer.measure(text, style)
2732 }
2733
2734 fn measure_line_width(
2735 &self,
2736 text: &crate::text::AnnotatedString,
2737 line_range: Range<usize>,
2738 style: &TextStyle,
2739 ) -> Option<f32> {
2740 self.line_width_calls.set(self.line_width_calls.get() + 1);
2741 MonospacedTextMeasurer.measure_line_width(text, line_range, style)
2742 }
2743
2744 fn measure_line_prefix_widths(
2745 &self,
2746 text: &crate::text::AnnotatedString,
2747 line_range: Range<usize>,
2748 style: &TextStyle,
2749 ) -> Option<TextLinePrefixWidths> {
2750 self.prefix_calls.set(self.prefix_calls.get() + 1);
2751 MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2752 }
2753
2754 fn get_offset_for_position(
2755 &self,
2756 text: &crate::text::AnnotatedString,
2757 style: &TextStyle,
2758 x: f32,
2759 y: f32,
2760 ) -> usize {
2761 MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2762 }
2763
2764 fn get_cursor_x_for_offset(
2765 &self,
2766 text: &crate::text::AnnotatedString,
2767 style: &TextStyle,
2768 offset: usize,
2769 ) -> f32 {
2770 MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2771 }
2772
2773 fn layout(
2774 &self,
2775 text: &crate::text::AnnotatedString,
2776 style: &TextStyle,
2777 ) -> TextLayoutResult {
2778 MonospacedTextMeasurer.layout(text, style)
2779 }
2780 }
2781
2782 impl TextMeasurer for CountingPreparedTextMeasurer {
2783 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2784 MonospacedTextMeasurer.measure(text, style)
2785 }
2786
2787 fn prepare_with_options_for_node(
2788 &self,
2789 _node_id: Option<NodeId>,
2790 text: &crate::text::AnnotatedString,
2791 style: &TextStyle,
2792 options: TextLayoutOptions,
2793 max_width: Option<f32>,
2794 ) -> PreparedTextLayout {
2795 self.prepare_calls.set(self.prepare_calls.get() + 1);
2796 MonospacedTextMeasurer.prepare_with_options(text, style, options, max_width)
2797 }
2798
2799 fn get_offset_for_position(
2800 &self,
2801 text: &crate::text::AnnotatedString,
2802 style: &TextStyle,
2803 x: f32,
2804 y: f32,
2805 ) -> usize {
2806 MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2807 }
2808
2809 fn get_cursor_x_for_offset(
2810 &self,
2811 text: &crate::text::AnnotatedString,
2812 style: &TextStyle,
2813 offset: usize,
2814 ) -> f32 {
2815 MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2816 }
2817
2818 fn layout(
2819 &self,
2820 text: &crate::text::AnnotatedString,
2821 style: &TextStyle,
2822 ) -> TextLayoutResult {
2823 MonospacedTextMeasurer.layout(text, style)
2824 }
2825 }
2826
2827 #[test]
2828 fn text_service_routes_measurement_through_current_measurer() {
2829 let _app_context = crate::render_state::app_context_test_scope();
2830 let service = TextService::from_measurer(Rc::new(MonospacedTextMeasurer));
2831 let text = crate::text::AnnotatedString::from("abc");
2832 let style = TextStyle::default();
2833
2834 let metrics = service.with_measurer(|measurer| measurer.measure(&text, &style));
2835
2836 assert!(metrics.width > 0.0);
2837 assert!(metrics.height > 0.0);
2838 }
2839
2840 #[test]
2841 fn text_service_caches_metrics_and_layouts_per_context() {
2842 let _app_context = crate::render_state::app_context_test_scope();
2843 let measure_calls = Rc::new(Cell::new(0));
2844 let layout_calls = Rc::new(Cell::new(0));
2845 let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2846 Rc::clone(&measure_calls),
2847 Rc::clone(&layout_calls),
2848 )));
2849 let text = crate::text::AnnotatedString::from("cached text");
2850 let style = TextStyle::default();
2851
2852 let first_metrics = service.measure(Some(7), &text, &style);
2853 let second_metrics = service.measure(Some(7), &text, &style);
2854 let first_layout = service.layout(&text, &style);
2855 let second_layout = service.layout(&text, &style);
2856
2857 assert_eq!(first_metrics, second_metrics);
2858 assert_eq!(first_layout.width, second_layout.width);
2859 assert_eq!(measure_calls.get(), 1);
2860 assert_eq!(layout_calls.get(), 1);
2861 }
2862
2863 #[test]
2864 fn text_service_reuses_metrics_cache_across_node_ids() {
2865 let _app_context = crate::render_state::app_context_test_scope();
2866 let measure_calls = Rc::new(Cell::new(0));
2867 let layout_calls = Rc::new(Cell::new(0));
2868 let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2869 Rc::clone(&measure_calls),
2870 Rc::clone(&layout_calls),
2871 )));
2872 let text = crate::text::AnnotatedString::from("same lazy item text");
2873 let style = TextStyle::default();
2874
2875 let first_metrics = service.measure(Some(7), &text, &style);
2876 let second_metrics = service.measure(Some(8), &text, &style);
2877
2878 assert_eq!(first_metrics, second_metrics);
2879 assert_eq!(measure_calls.get(), 1);
2880 }
2881
2882 #[test]
2883 fn text_service_reuses_prepared_layout_cache_across_node_ids() {
2884 let _app_context = crate::render_state::app_context_test_scope();
2885 let prepare_calls = Rc::new(Cell::new(0));
2886 let service = TextService::from_measurer(Rc::new(CountingPreparedTextMeasurer::new(
2887 Rc::clone(&prepare_calls),
2888 )));
2889 let text = crate::text::AnnotatedString::from("same prepared lazy item text");
2890 let style = TextStyle::default();
2891 let options = TextLayoutOptions::default();
2892
2893 let first = service.prepare_with_options(Some(9), &text, &style, options, Some(120.0));
2894 let second = service.prepare_with_options(Some(10), &text, &style, options, Some(120.0));
2895
2896 assert_eq!(first.metrics, second.metrics);
2897 assert_eq!(prepare_calls.get(), 1);
2898 }
2899
2900 #[test]
2901 fn text_service_clears_caches_when_measurer_changes() {
2902 let _app_context = crate::render_state::app_context_test_scope();
2903 let first_measure_calls = Rc::new(Cell::new(0));
2904 let second_measure_calls = Rc::new(Cell::new(0));
2905 let layout_calls = Rc::new(Cell::new(0));
2906 let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2907 Rc::clone(&first_measure_calls),
2908 Rc::clone(&layout_calls),
2909 )));
2910 let text = crate::text::AnnotatedString::from("cached text");
2911 let style = TextStyle::default();
2912
2913 let _ = service.measure(None, &text, &style);
2914 let _ = service.measure(None, &text, &style);
2915 service.set_measurer(Rc::new(CountingTextMeasurer::new(
2916 Rc::clone(&second_measure_calls),
2917 Rc::clone(&layout_calls),
2918 )));
2919 let _ = service.measure(None, &text, &style);
2920
2921 assert_eq!(first_measure_calls.get(), 1);
2922 assert_eq!(second_measure_calls.get(), 1);
2923 }
2924
2925 #[test]
2926 fn text_wrapping_uses_prefix_widths_without_subsequence_measurement() {
2927 let _app_context = crate::render_state::app_context_test_scope();
2928 let prefix_calls = Rc::new(Cell::new(0));
2929 let subsequence_calls = Rc::new(Cell::new(0));
2930 set_text_measurer(PrefixWidthCountingMeasurer::new(
2931 Rc::clone(&prefix_calls),
2932 Rc::clone(&subsequence_calls),
2933 ));
2934 let style = TextStyle {
2935 span_style: crate::text::SpanStyle {
2936 font_size: TextUnit::Sp(10.0),
2937 ..Default::default()
2938 },
2939 ..Default::default()
2940 };
2941 let options = TextLayoutOptions {
2942 overflow: TextOverflow::Clip,
2943 soft_wrap: true,
2944 max_lines: usize::MAX,
2945 min_lines: 1,
2946 };
2947 let text = crate::text::AnnotatedString::from("word ".repeat(80).as_str());
2948
2949 let prepared = prepare_text_layout(&text, &style, options, Some(80.0));
2950
2951 assert!(prepared.metrics.line_count > 1);
2952 assert!(
2953 prefix_calls.get() > 0,
2954 "wrapping should request a line prefix width plan"
2955 );
2956 assert_eq!(
2957 subsequence_calls.get(),
2958 0,
2959 "prefix-capable wrapping should not probe candidate substrings"
2960 );
2961 }
2962
2963 #[test]
2964 fn text_wrapping_skips_prefix_widths_when_fit_probe_says_line_fits() {
2965 let _app_context = crate::render_state::app_context_test_scope();
2966 let line_width_calls = Rc::new(Cell::new(0));
2967 let prefix_calls = Rc::new(Cell::new(0));
2968 set_text_measurer(FitProbeCountingMeasurer::new(
2969 Rc::clone(&line_width_calls),
2970 Rc::clone(&prefix_calls),
2971 ));
2972 let style = TextStyle {
2973 span_style: crate::text::SpanStyle {
2974 font_size: TextUnit::Sp(10.0),
2975 ..Default::default()
2976 },
2977 ..Default::default()
2978 };
2979 let text = crate::text::AnnotatedString::from("fits without per-glyph prefix widths");
2980
2981 let prepared =
2982 prepare_text_layout(&text, &style, TextLayoutOptions::default(), Some(800.0));
2983
2984 assert_eq!(prepared.metrics.line_count, 1);
2985 assert_eq!(line_width_calls.get(), 1);
2986 assert_eq!(
2987 prefix_calls.get(),
2988 0,
2989 "fitting lines should not allocate prefix-width plans"
2990 );
2991 }
2992
2993 #[test]
2994 fn prepare_text_layout_uses_line_height_without_full_text_measurement() {
2995 let _app_context = crate::render_state::app_context_test_scope();
2996 let measure_calls = Rc::new(Cell::new(0));
2997 let line_height_calls = Rc::new(Cell::new(0));
2998 let measurer = LineHeightCountingMeasurer::new(
2999 Rc::clone(&measure_calls),
3000 Rc::clone(&line_height_calls),
3001 );
3002 let text = crate::text::AnnotatedString::from(
3003 "one two three four five six seven eight nine ten eleven twelve",
3004 );
3005
3006 let prepared = prepare_text_layout_with_measurer_for_node(
3007 &measurer,
3008 Some(7),
3009 &text,
3010 &TextStyle::default(),
3011 TextLayoutOptions::default(),
3012 Some(96.0),
3013 );
3014
3015 assert!(prepared.metrics.height > 0.0);
3016 assert_eq!(line_height_calls.get(), 1);
3017 assert_eq!(
3018 measure_calls.get(),
3019 0,
3020 "line-height lookup must not re-measure the whole paragraph"
3021 );
3022 }
3023
3024 fn style_with_line_break(line_break: LineBreak) -> TextStyle {
3025 TextStyle {
3026 span_style: crate::text::SpanStyle {
3027 font_size: TextUnit::Sp(10.0),
3028 ..Default::default()
3029 },
3030 paragraph_style: ParagraphStyle {
3031 line_break,
3032 ..Default::default()
3033 },
3034 }
3035 }
3036
3037 fn style_with_hyphens(hyphens: Hyphens) -> TextStyle {
3038 TextStyle {
3039 span_style: crate::text::SpanStyle {
3040 font_size: TextUnit::Sp(10.0),
3041 ..Default::default()
3042 },
3043 paragraph_style: ParagraphStyle {
3044 hyphens,
3045 ..Default::default()
3046 },
3047 }
3048 }
3049
3050 fn assert_f32_close(actual: f32, expected: f32) {
3051 assert!(
3052 (actual - expected).abs() <= 0.01,
3053 "actual={actual}, expected={expected}"
3054 );
3055 }
3056
3057 #[test]
3058 fn text_layout_options_wraps_and_limits_lines() {
3059 let _app_context = crate::render_state::app_context_test_scope();
3060 let style = TextStyle {
3061 span_style: crate::text::SpanStyle {
3062 font_size: TextUnit::Sp(10.0),
3063 ..Default::default()
3064 },
3065 ..Default::default()
3066 };
3067 let options = TextLayoutOptions {
3068 overflow: TextOverflow::Clip,
3069 soft_wrap: true,
3070 max_lines: 2,
3071 min_lines: 1,
3072 };
3073
3074 let prepared = prepare_text_layout(
3075 &crate::text::AnnotatedString::from("A B C D E F"),
3076 &style,
3077 options,
3078 Some(24.0), );
3080
3081 assert!(prepared.did_overflow);
3082 assert!(prepared.metrics.line_count <= 2);
3083 }
3084
3085 #[test]
3086 fn text_layout_options_end_ellipsis_applies() {
3087 let _app_context = crate::render_state::app_context_test_scope();
3088 let style = TextStyle {
3089 span_style: crate::text::SpanStyle {
3090 font_size: TextUnit::Sp(10.0),
3091 ..Default::default()
3092 },
3093 ..Default::default()
3094 };
3095 let options = TextLayoutOptions {
3096 overflow: TextOverflow::Ellipsis,
3097 soft_wrap: false,
3098 max_lines: 1,
3099 min_lines: 1,
3100 };
3101
3102 let prepared = prepare_text_layout(
3103 &crate::text::AnnotatedString::from("Long long line"),
3104 &style,
3105 options,
3106 Some(20.0),
3107 );
3108 assert!(prepared.did_overflow);
3109 assert!(prepared.text.text.contains(ELLIPSIS));
3110 }
3111
3112 #[test]
3113 fn text_layout_options_visible_keeps_full_text() {
3114 let _app_context = crate::render_state::app_context_test_scope();
3115 let style = TextStyle {
3116 span_style: crate::text::SpanStyle {
3117 font_size: TextUnit::Sp(10.0),
3118 ..Default::default()
3119 },
3120 ..Default::default()
3121 };
3122 let options = TextLayoutOptions {
3123 overflow: TextOverflow::Visible,
3124 soft_wrap: false,
3125 max_lines: 1,
3126 min_lines: 1,
3127 };
3128
3129 let input = "This should remain unchanged";
3130 let prepared = prepare_text_layout(
3131 &crate::text::AnnotatedString::from(input),
3132 &style,
3133 options,
3134 Some(10.0),
3135 );
3136 assert_eq!(prepared.text.text, input);
3137 }
3138
3139 #[test]
3140 fn text_layout_options_respects_min_lines() {
3141 let _app_context = crate::render_state::app_context_test_scope();
3142 let style = TextStyle {
3143 span_style: crate::text::SpanStyle {
3144 font_size: TextUnit::Sp(10.0),
3145 ..Default::default()
3146 },
3147 ..Default::default()
3148 };
3149 let options = TextLayoutOptions {
3150 overflow: TextOverflow::Clip,
3151 soft_wrap: true,
3152 max_lines: 4,
3153 min_lines: 3,
3154 };
3155
3156 let prepared = prepare_text_layout(
3157 &crate::text::AnnotatedString::from("short"),
3158 &style,
3159 options,
3160 Some(100.0),
3161 );
3162 assert_eq!(prepared.metrics.line_count, 3);
3163 }
3164
3165 #[test]
3166 fn text_layout_options_middle_ellipsis_for_single_line() {
3167 let _app_context = crate::render_state::app_context_test_scope();
3168 let style = TextStyle {
3169 span_style: crate::text::SpanStyle {
3170 font_size: TextUnit::Sp(10.0),
3171 ..Default::default()
3172 },
3173 ..Default::default()
3174 };
3175 let options = TextLayoutOptions {
3176 overflow: TextOverflow::MiddleEllipsis,
3177 soft_wrap: false,
3178 max_lines: 1,
3179 min_lines: 1,
3180 };
3181
3182 let prepared = prepare_text_layout(
3183 &crate::text::AnnotatedString::from("abcdefghijk"),
3184 &style,
3185 options,
3186 Some(24.0),
3187 );
3188 assert!(prepared.text.text.contains(ELLIPSIS));
3189 assert!(prepared.did_overflow);
3190 }
3191
3192 #[test]
3193 fn text_layout_options_scale_down_fits_without_rewriting_text() {
3194 let _app_context = crate::render_state::app_context_test_scope();
3195 let style = TextStyle {
3196 span_style: crate::text::SpanStyle {
3197 font_size: TextUnit::Sp(20.0),
3198 ..Default::default()
3199 },
3200 ..Default::default()
3201 };
3202 let options = TextLayoutOptions {
3203 overflow: TextOverflow::ScaleDown {
3204 min_font_size_sp: 10.0,
3205 },
3206 soft_wrap: false,
3207 max_lines: 1,
3208 min_lines: 1,
3209 };
3210
3211 let prepared = prepare_text_layout(
3212 &crate::text::AnnotatedString::from("ABCDE"),
3213 &style,
3214 options,
3215 Some(36.0),
3216 );
3217
3218 assert_eq!(prepared.text.text, "ABCDE");
3219 assert!(prepared.metrics.width <= 36.0 + WRAP_EPSILON);
3220 assert!(!prepared.did_overflow);
3221 let visual_font_size = prepared.visual_style.resolve_font_size(14.0);
3222 assert!(visual_font_size < 20.0);
3223 assert!(visual_font_size >= 10.0);
3224 }
3225
3226 #[test]
3227 fn text_layout_options_scale_down_scales_root_shadow() {
3228 let _app_context = crate::render_state::app_context_test_scope();
3229 let style = TextStyle {
3230 span_style: crate::text::SpanStyle {
3231 font_size: TextUnit::Sp(20.0),
3232 shadow: Some(crate::text::Shadow {
3233 color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
3234 offset: crate::modifier::Point::new(8.0, 4.0),
3235 blur_radius: 6.0,
3236 }),
3237 ..Default::default()
3238 },
3239 ..Default::default()
3240 };
3241 let options = TextLayoutOptions {
3242 overflow: TextOverflow::ScaleDown {
3243 min_font_size_sp: 10.0,
3244 },
3245 soft_wrap: false,
3246 max_lines: 1,
3247 min_lines: 1,
3248 };
3249
3250 let prepared = prepare_text_layout(
3251 &crate::text::AnnotatedString::from("ABCDE"),
3252 &style,
3253 options,
3254 Some(36.0),
3255 );
3256
3257 let font_scale = prepared.visual_style.resolve_font_size(14.0) / 20.0;
3258 let shadow = prepared
3259 .visual_style
3260 .span_style
3261 .shadow
3262 .expect("scaled style should retain shadow");
3263 assert_f32_close(shadow.offset.x, 8.0 * font_scale);
3264 assert_f32_close(shadow.offset.y, 4.0 * font_scale);
3265 assert_f32_close(shadow.blur_radius, 6.0 * font_scale);
3266 }
3267
3268 #[test]
3269 fn text_layout_options_scale_down_stops_at_minimum_and_clips() {
3270 let _app_context = crate::render_state::app_context_test_scope();
3271 let style = TextStyle {
3272 span_style: crate::text::SpanStyle {
3273 font_size: TextUnit::Sp(20.0),
3274 ..Default::default()
3275 },
3276 ..Default::default()
3277 };
3278 let options = TextLayoutOptions {
3279 overflow: TextOverflow::ScaleDown {
3280 min_font_size_sp: 10.0,
3281 },
3282 soft_wrap: false,
3283 max_lines: 1,
3284 min_lines: 1,
3285 };
3286
3287 let prepared = prepare_text_layout(
3288 &crate::text::AnnotatedString::from("ABCDEFGHIJ"),
3289 &style,
3290 options,
3291 Some(12.0),
3292 );
3293
3294 assert_eq!(prepared.text.text, "ABCDEFGHIJ");
3295 assert!(prepared.did_overflow);
3296 assert_eq!(prepared.metrics.width, 12.0);
3297 assert_eq!(prepared.visual_style.resolve_font_size(14.0), 10.0);
3298 }
3299
3300 #[test]
3301 fn scale_annotated_font_sizes_borrows_when_spans_need_no_scaling() {
3302 let _app_context = crate::render_state::app_context_test_scope();
3303 let plain = crate::text::AnnotatedString::from("plain");
3304 assert!(matches!(
3305 scale_annotated_font_sizes(&plain, FontScaleCurve::linear(0.5)),
3306 std::borrow::Cow::Borrowed(_)
3307 ));
3308
3309 let colored = crate::text::annotated_string::Builder::new()
3310 .push_style(crate::text::SpanStyle {
3311 color: Some(crate::modifier::Color(1.0, 0.0, 0.0, 1.0)),
3312 ..Default::default()
3313 })
3314 .append("colored")
3315 .pop()
3316 .to_annotated_string();
3317 assert!(matches!(
3318 scale_annotated_font_sizes(&colored, FontScaleCurve::linear(0.5)),
3319 std::borrow::Cow::Borrowed(_)
3320 ));
3321 }
3322
3323 #[test]
3324 fn scale_annotated_font_sizes_scales_span_shadow_geometry() {
3325 let _app_context = crate::render_state::app_context_test_scope();
3326 let text = crate::text::annotated_string::Builder::new()
3327 .push_style(crate::text::SpanStyle {
3328 shadow: Some(crate::text::Shadow {
3329 color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
3330 offset: crate::modifier::Point::new(6.0, 2.0),
3331 blur_radius: 4.0,
3332 }),
3333 ..Default::default()
3334 })
3335 .append("shadow")
3336 .pop()
3337 .to_annotated_string();
3338
3339 let scaled = scale_annotated_font_sizes(&text, FontScaleCurve::linear(0.5));
3340 let std::borrow::Cow::Owned(scaled) = scaled else {
3341 panic!("shadowed span should be scaled into owned text");
3342 };
3343 let shadow = scaled.span_styles[0]
3344 .item
3345 .shadow
3346 .expect("scaled span should retain shadow");
3347 assert_f32_close(shadow.offset.x, 3.0);
3348 assert_f32_close(shadow.offset.y, 1.0);
3349 assert_f32_close(shadow.blur_radius, 2.0);
3350 }
3351
3352 #[test]
3353 fn text_layout_options_does_not_wrap_on_tiny_width_delta() {
3354 let _app_context = crate::render_state::app_context_test_scope();
3355 let style = TextStyle {
3356 span_style: crate::text::SpanStyle {
3357 font_size: TextUnit::Sp(10.0),
3358 ..Default::default()
3359 },
3360 ..Default::default()
3361 };
3362 let options = TextLayoutOptions {
3363 overflow: TextOverflow::Clip,
3364 soft_wrap: true,
3365 max_lines: usize::MAX,
3366 min_lines: 1,
3367 };
3368
3369 let text = "if counter % 2 == 0";
3370 let exact_width = measure_text(&crate::text::AnnotatedString::from(text), &style).width;
3371 let prepared = prepare_text_layout(
3372 &crate::text::AnnotatedString::from(text),
3373 &style,
3374 options,
3375 Some(exact_width - 0.1),
3376 );
3377
3378 assert!(
3379 !prepared.text.text.contains('\n'),
3380 "unexpected line split: {:?}",
3381 prepared.text
3382 );
3383 }
3384
3385 #[test]
3386 fn line_break_mode_changes_wrap_strategy_contract() {
3387 let _app_context = crate::render_state::app_context_test_scope();
3388 let text = "This is an example text";
3389 let options = TextLayoutOptions {
3390 overflow: TextOverflow::Clip,
3391 soft_wrap: true,
3392 max_lines: usize::MAX,
3393 min_lines: 1,
3394 };
3395
3396 let simple = prepare_text_layout(
3397 &crate::text::AnnotatedString::from(text),
3398 &style_with_line_break(LineBreak::Simple),
3399 options,
3400 Some(120.0),
3401 );
3402 let heading = prepare_text_layout(
3403 &crate::text::AnnotatedString::from(text),
3404 &style_with_line_break(LineBreak::Heading),
3405 options,
3406 Some(120.0),
3407 );
3408 let paragraph = prepare_text_layout(
3409 &crate::text::AnnotatedString::from(text),
3410 &style_with_line_break(LineBreak::Paragraph),
3411 options,
3412 Some(50.0),
3413 );
3414
3415 assert_eq!(
3416 simple.text.text.lines().collect::<Vec<_>>(),
3417 vec!["This is an example", "text"]
3418 );
3419 assert_eq!(
3420 heading.text.text.lines().collect::<Vec<_>>(),
3421 vec!["This is an", "example text"]
3422 );
3423 assert_eq!(
3424 paragraph.text.text.lines().collect::<Vec<_>>(),
3425 vec!["This", "is an", "example", "text"]
3426 );
3427 }
3428
3429 #[test]
3430 fn hyphens_mode_changes_wrap_strategy_contract() {
3431 let _app_context = crate::render_state::app_context_test_scope();
3432 let text = "Transformation";
3433 let options = TextLayoutOptions {
3434 overflow: TextOverflow::Clip,
3435 soft_wrap: true,
3436 max_lines: usize::MAX,
3437 min_lines: 1,
3438 };
3439
3440 let auto = prepare_text_layout(
3441 &crate::text::AnnotatedString::from(text),
3442 &style_with_hyphens(Hyphens::Auto),
3443 options,
3444 Some(24.0),
3445 );
3446 let none = prepare_text_layout(
3447 &crate::text::AnnotatedString::from(text),
3448 &style_with_hyphens(Hyphens::None),
3449 options,
3450 Some(24.0),
3451 );
3452
3453 assert_eq!(
3454 auto.text.text.lines().collect::<Vec<_>>(),
3455 vec!["Tran", "sfor", "ma", "tion"]
3456 );
3457 assert_eq!(
3458 none.text.text.lines().collect::<Vec<_>>(),
3459 vec!["Tran", "sfor", "mati", "on"]
3460 );
3461 assert!(
3462 !auto.text.text.contains('-'),
3463 "automatic hyphenation should influence breaks without mutating source text content"
3464 );
3465 }
3466
3467 #[test]
3468 fn hyphens_auto_uses_measurer_hyphen_contract_when_valid() {
3469 let _app_context = crate::render_state::app_context_test_scope();
3470 let text = "Transformation";
3471 let style = style_with_hyphens(Hyphens::Auto);
3472 let options = TextLayoutOptions {
3473 overflow: TextOverflow::Clip,
3474 soft_wrap: true,
3475 max_lines: usize::MAX,
3476 min_lines: 1,
3477 };
3478
3479 let prepared = prepare_text_layout_fallback(
3480 &ContractBreakMeasurer { retreat: 1 },
3481 &crate::text::AnnotatedString::from(text),
3482 &style,
3483 options,
3484 Some(24.0),
3485 );
3486
3487 assert_eq!(
3488 prepared.text.text.lines().collect::<Vec<_>>(),
3489 vec!["Tra", "nsf", "orm", "ati", "on"]
3490 );
3491 }
3492
3493 #[test]
3494 fn hyphens_auto_falls_back_when_measurer_hyphen_contract_is_invalid() {
3495 let _app_context = crate::render_state::app_context_test_scope();
3496 let text = "Transformation";
3497 let style = style_with_hyphens(Hyphens::Auto);
3498 let options = TextLayoutOptions {
3499 overflow: TextOverflow::Clip,
3500 soft_wrap: true,
3501 max_lines: usize::MAX,
3502 min_lines: 1,
3503 };
3504
3505 let prepared = prepare_text_layout_fallback(
3506 &ContractBreakMeasurer { retreat: 10 },
3507 &crate::text::AnnotatedString::from(text),
3508 &style,
3509 options,
3510 Some(24.0),
3511 );
3512
3513 assert_eq!(
3514 prepared.text.text.lines().collect::<Vec<_>>(),
3515 vec!["Tran", "sfor", "ma", "tion"]
3516 );
3517 }
3518
3519 #[test]
3520 fn transformed_text_keeps_span_ranges_within_display_bounds() {
3521 let _app_context = crate::render_state::app_context_test_scope();
3522 let style = TextStyle {
3523 span_style: crate::text::SpanStyle {
3524 font_size: TextUnit::Sp(10.0),
3525 ..Default::default()
3526 },
3527 ..Default::default()
3528 };
3529 let options = TextLayoutOptions {
3530 overflow: TextOverflow::Ellipsis,
3531 soft_wrap: false,
3532 max_lines: 1,
3533 min_lines: 1,
3534 };
3535 let annotated = crate::text::AnnotatedString::builder()
3536 .push_style(crate::text::SpanStyle {
3537 font_weight: Some(crate::text::FontWeight::BOLD),
3538 ..Default::default()
3539 })
3540 .append("Styled overflow text sample")
3541 .pop()
3542 .to_annotated_string();
3543
3544 let prepared = prepare_text_layout(&annotated, &style, options, Some(40.0));
3545 assert!(prepared.did_overflow);
3546 for span in &prepared.text.span_styles {
3547 assert!(span.range.start < span.range.end);
3548 assert!(span.range.end <= prepared.text.text.len());
3549 assert!(prepared.text.text.is_char_boundary(span.range.start));
3550 assert!(prepared.text.text.is_char_boundary(span.range.end));
3551 }
3552 }
3553
3554 #[test]
3555 fn a_word_that_fits_stays_on_the_line_when_the_space_after_it_fits_too() {
3556 let _app_context = crate::render_state::app_context_test_scope();
3564 let style = TextStyle {
3565 span_style: crate::text::SpanStyle {
3566 font_size: TextUnit::Sp(10.0),
3567 ..Default::default()
3568 },
3569 ..Default::default()
3570 };
3571 let options = TextLayoutOptions {
3572 overflow: TextOverflow::Clip,
3573 soft_wrap: true,
3574 max_lines: usize::MAX,
3575 min_lines: 1,
3576 };
3577 let width_of = |text: &str| {
3578 measure_text_with_options(
3579 &crate::text::AnnotatedString::from(text.to_string()),
3580 &style,
3581 options,
3582 None,
3583 )
3584 .width
3585 };
3586 let fits = width_of("aa bb ");
3588 let overflows = width_of("aa bb c");
3589 assert!(overflows > fits, "the fixture needs a real gap here");
3590 let max_width = (fits + overflows) * 0.5;
3591
3592 let prepared = prepare_text_layout(
3593 &crate::text::AnnotatedString::from("aa bb cc".to_string()),
3594 &style,
3595 options,
3596 Some(max_width),
3597 );
3598 assert_eq!(prepared.text.text, "aa bb\ncc");
3599 }
3600
3601 #[test]
3602 fn wrapped_text_splits_styles_around_inserted_newlines() {
3603 let _app_context = crate::render_state::app_context_test_scope();
3604 let style = TextStyle {
3605 span_style: crate::text::SpanStyle {
3606 font_size: TextUnit::Sp(10.0),
3607 ..Default::default()
3608 },
3609 ..Default::default()
3610 };
3611 let options = TextLayoutOptions {
3612 overflow: TextOverflow::Clip,
3613 soft_wrap: true,
3614 max_lines: usize::MAX,
3615 min_lines: 1,
3616 };
3617 let annotated = crate::text::AnnotatedString::builder()
3618 .push_style(crate::text::SpanStyle {
3619 text_decoration: Some(crate::text::TextDecoration::UNDERLINE),
3620 ..Default::default()
3621 })
3622 .append("Wrapped style text example")
3623 .pop()
3624 .to_annotated_string();
3625
3626 let prepared = prepare_text_layout(&annotated, &style, options, Some(32.0));
3627 assert!(prepared.text.text.contains('\n'));
3628 assert!(!prepared.text.span_styles.is_empty());
3629 for span in &prepared.text.span_styles {
3630 assert!(span.range.end <= prepared.text.text.len());
3631 }
3632 }
3633
3634 #[test]
3635 fn mixed_font_size_segments_wrap_without_truncation() {
3636 let _app_context = crate::render_state::app_context_test_scope();
3637 let style = TextStyle {
3638 span_style: crate::text::SpanStyle {
3639 font_size: TextUnit::Sp(14.0),
3640 ..Default::default()
3641 },
3642 ..Default::default()
3643 };
3644 let options = TextLayoutOptions {
3645 overflow: TextOverflow::Clip,
3646 soft_wrap: true,
3647 max_lines: usize::MAX,
3648 min_lines: 1,
3649 };
3650 let annotated = crate::text::AnnotatedString::builder()
3651 .append("You can also ")
3652 .push_style(crate::text::SpanStyle {
3653 font_size: TextUnit::Sp(22.0),
3654 ..Default::default()
3655 })
3656 .append("change font size")
3657 .pop()
3658 .append(" dynamically mid-sentence!")
3659 .to_annotated_string();
3660
3661 let prepared = prepare_text_layout(&annotated, &style, options, Some(260.0));
3662 assert!(prepared.text.text.contains('\n'));
3663 assert!(prepared.text.text.contains("mid-sentence!"));
3664 assert!(!prepared.did_overflow);
3665 }
3666}