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