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