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