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