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