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