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