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