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