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