1use std::{
2 borrow::Cow,
3 cell::{Cell, RefCell},
4 collections::{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_PREPARED_CACHE_CAPACITY: usize = 1024;
33const TEXT_LAYOUT_TELEMETRY_ENV: &str = "CRANPOSE_TEXT_LAYOUT_TELEMETRY";
34
35fn text_layout_telemetry_enabled() -> bool {
36 cranpose_core::env_flag!(TEXT_LAYOUT_TELEMETRY_ENV)
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct TextMetrics {
41 pub width: f32,
42 pub height: f32,
43 pub line_height: f32,
45 pub line_count: usize,
47}
48
49#[derive(Clone, Debug, PartialEq)]
50pub struct PreparedTextLayout {
51 pub text: Rc<crate::text::AnnotatedString>,
53 pub visual_style: TextStyle,
54 pub metrics: TextMetrics,
55 pub did_overflow: bool,
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub struct TextLinePrefixWidths {
60 prefix_widths: Vec<f32>,
61 separator_before: Vec<f32>,
62 non_empty_overhang: f32,
63}
64
65impl TextLinePrefixWidths {
66 pub fn from_parts(
67 prefix_widths: Vec<f32>,
68 separator_before: Vec<f32>,
69 non_empty_overhang: f32,
70 ) -> Option<Self> {
71 if prefix_widths.is_empty() || prefix_widths.len() != separator_before.len() + 1 {
72 return None;
73 }
74 if prefix_widths
75 .iter()
76 .chain(separator_before.iter())
77 .any(|value| !value.is_finite())
78 {
79 return None;
80 }
81 let non_empty_overhang = non_empty_overhang.max(0.0);
82 if !non_empty_overhang.is_finite() {
83 return None;
84 }
85 Some(Self {
86 prefix_widths,
87 separator_before,
88 non_empty_overhang,
89 })
90 }
91
92 pub fn monospaced(char_count: usize, char_width: f32, letter_spacing: f32) -> Option<Self> {
93 if !char_width.is_finite() || !letter_spacing.is_finite() {
94 return None;
95 }
96 let char_width = char_width.max(0.0);
97 let letter_spacing = letter_spacing.max(0.0);
98 let mut prefix_widths = Vec::with_capacity(char_count + 1);
99 let mut separator_before = Vec::with_capacity(char_count);
100 let mut width = 0.0f32;
101 prefix_widths.push(width);
102 for _ in 0..char_count {
103 separator_before.push(0.0);
104 width += char_width + letter_spacing;
105 prefix_widths.push(width);
106 }
107 Self::from_parts(prefix_widths, separator_before, 0.0)
108 }
109
110 pub fn char_count(&self) -> usize {
111 self.separator_before.len()
112 }
113
114 pub fn width_for_char_range(&self, start: usize, end: usize) -> Option<f32> {
115 if start > end || end > self.char_count() {
116 return None;
117 }
118 if start == end {
119 return Some(0.0);
120 }
121 let separator = self.separator_before.get(start).copied().unwrap_or(0.0);
122 Some(
123 (self.prefix_widths[end] - self.prefix_widths[start] - separator).max(0.0)
124 + self.non_empty_overhang,
125 )
126 }
127}
128
129pub trait TextMeasurer: 'static {
130 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
131
132 fn measure_for_node(
133 &self,
134 node_id: Option<NodeId>,
135 text: &crate::text::AnnotatedString,
136 style: &TextStyle,
137 ) -> TextMetrics {
138 let _ = node_id;
139 self.measure(text, style)
140 }
141
142 fn measure_subsequence(
143 &self,
144 text: &crate::text::AnnotatedString,
145 range: Range<usize>,
146 style: &TextStyle,
147 ) -> TextMetrics {
148 self.measure(&text.subsequence(range), style)
149 }
150
151 fn measure_subsequence_for_node(
152 &self,
153 node_id: Option<NodeId>,
154 text: &crate::text::AnnotatedString,
155 range: Range<usize>,
156 style: &TextStyle,
157 ) -> TextMetrics {
158 let _ = node_id;
159 self.measure_subsequence(text, range, style)
160 }
161
162 fn measure_line_prefix_widths(
163 &self,
164 text: &crate::text::AnnotatedString,
165 line_range: Range<usize>,
166 style: &TextStyle,
167 ) -> Option<TextLinePrefixWidths> {
168 let _ = text;
169 let _ = line_range;
170 let _ = style;
171 None
172 }
173
174 fn measure_line_width(
175 &self,
176 text: &crate::text::AnnotatedString,
177 line_range: Range<usize>,
178 style: &TextStyle,
179 ) -> Option<f32> {
180 let _ = text;
181 let _ = line_range;
182 let _ = style;
183 None
184 }
185
186 fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
187 self.measure(text, style).line_height
188 }
189
190 fn glyph_line_box(&self, style: &TextStyle) -> Option<(f32, f32)> {
199 let _ = style;
200 None
201 }
202
203 fn first_baseline(&self, style: &TextStyle) -> Option<f32> {
209 let _ = style;
210 None
211 }
212
213 fn line_box(&self, style: &TextStyle) -> Option<crate::text::LineBox> {
220 let _ = style;
221 None
222 }
223
224 fn line_height_for_node(
225 &self,
226 node_id: Option<NodeId>,
227 text: &crate::text::AnnotatedString,
228 style: &TextStyle,
229 ) -> f32 {
230 let _ = node_id;
231 self.line_height(text, style)
232 }
233
234 fn get_offset_for_position(
235 &self,
236 text: &crate::text::AnnotatedString,
237 style: &TextStyle,
238 x: f32,
239 y: f32,
240 ) -> usize;
241
242 fn get_cursor_x_for_offset(
243 &self,
244 text: &crate::text::AnnotatedString,
245 style: &TextStyle,
246 offset: usize,
247 ) -> f32;
248
249 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
250
251 fn choose_auto_hyphen_break(
257 &self,
258 _line: &str,
259 _style: &TextStyle,
260 _segment_start_char: usize,
261 _measured_break_char: usize,
262 ) -> Option<usize> {
263 None
264 }
265
266 fn measure_with_options(
267 &self,
268 text: &crate::text::AnnotatedString,
269 style: &TextStyle,
270 options: TextLayoutOptions,
271 max_width: Option<f32>,
272 ) -> TextMetrics {
273 self.prepare_with_options(text, style, options, max_width)
274 .metrics
275 }
276
277 fn measure_with_options_for_node(
278 &self,
279 node_id: Option<NodeId>,
280 text: &crate::text::AnnotatedString,
281 style: &TextStyle,
282 options: TextLayoutOptions,
283 max_width: Option<f32>,
284 ) -> TextMetrics {
285 self.prepare_with_options_for_node(node_id, text, style, options, max_width)
286 .metrics
287 }
288
289 fn prepare_with_options(
290 &self,
291 text: &crate::text::AnnotatedString,
292 style: &TextStyle,
293 options: TextLayoutOptions,
294 max_width: Option<f32>,
295 ) -> PreparedTextLayout {
296 self.prepare_with_options_fallback(text, style, options, max_width)
297 }
298
299 fn prepare_with_options_for_node(
300 &self,
301 node_id: Option<NodeId>,
302 text: &crate::text::AnnotatedString,
303 style: &TextStyle,
304 options: TextLayoutOptions,
305 max_width: Option<f32>,
306 ) -> PreparedTextLayout {
307 prepare_text_layout_with_measurer_for_node(self, node_id, text, style, options, max_width)
308 }
309
310 fn prepare_with_options_fallback(
311 &self,
312 text: &crate::text::AnnotatedString,
313 style: &TextStyle,
314 options: TextLayoutOptions,
315 max_width: Option<f32>,
316 ) -> PreparedTextLayout {
317 prepare_text_layout_fallback(self, text, style, options, max_width)
318 }
319}
320
321#[derive(Default)]
322struct MonospacedTextMeasurer;
323
324impl MonospacedTextMeasurer {
325 const DEFAULT_SIZE: f32 = 14.0;
326 const CHAR_WIDTH_RATIO: f32 = 0.6;
327
328 fn get_metrics(style: &TextStyle) -> (f32, f32) {
329 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
330 let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
331 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
332 (
333 (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
334 line_height,
335 )
336 }
337}
338
339impl TextMeasurer for MonospacedTextMeasurer {
340 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
341 let (char_width, line_height) = Self::get_metrics(style);
342
343 let lines: Vec<&str> = text.text.split('\n').collect();
344 let line_count = lines.len().max(1);
345
346 let width = lines
347 .iter()
348 .map(|line| line.chars().count() as f32 * char_width)
349 .fold(0.0_f32, f32::max);
350
351 TextMetrics {
352 width,
353 height: line_count as f32 * line_height,
354 line_height,
355 line_count,
356 }
357 }
358
359 fn measure_subsequence(
360 &self,
361 text: &crate::text::AnnotatedString,
362 range: Range<usize>,
363 style: &TextStyle,
364 ) -> TextMetrics {
365 let (char_width, line_height) = Self::get_metrics(style);
366 let slice = &text.text[range];
367 let line_count = slice.split('\n').count().max(1);
368 let width = slice
369 .split('\n')
370 .map(|line| line.chars().count() as f32 * char_width)
371 .fold(0.0_f32, f32::max);
372
373 TextMetrics {
374 width,
375 height: line_count as f32 * line_height,
376 line_height,
377 line_count,
378 }
379 }
380
381 fn measure_line_prefix_widths(
382 &self,
383 text: &crate::text::AnnotatedString,
384 line_range: Range<usize>,
385 style: &TextStyle,
386 ) -> Option<TextLinePrefixWidths> {
387 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
388 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE);
389 TextLinePrefixWidths::monospaced(
390 text.text[line_range].chars().count(),
391 font_size * Self::CHAR_WIDTH_RATIO,
392 letter_spacing,
393 )
394 }
395
396 fn measure_line_width(
397 &self,
398 text: &crate::text::AnnotatedString,
399 line_range: Range<usize>,
400 style: &TextStyle,
401 ) -> Option<f32> {
402 Some(self.measure_subsequence(text, line_range, style).width)
403 }
404
405 fn line_height(&self, _text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
406 let (_, line_height) = Self::get_metrics(style);
407 line_height
408 }
409
410 fn get_offset_for_position(
411 &self,
412 text: &crate::text::AnnotatedString,
413 style: &TextStyle,
414 x: f32,
415 y: f32,
416 ) -> usize {
417 let (char_width, line_height) = Self::get_metrics(style);
418
419 if text.text.is_empty() {
420 return 0;
421 }
422
423 let line_index = (y / line_height).floor().max(0.0) as usize;
424 let lines: Vec<&str> = text.text.split('\n').collect();
425 let target_line = line_index.min(lines.len().saturating_sub(1));
426
427 let mut line_start_byte = 0;
428 for line in lines.iter().take(target_line) {
429 line_start_byte += line.len() + 1;
430 }
431
432 let line_text = lines.get(target_line).unwrap_or(&"");
433 let char_index = (x / char_width).round() as usize;
434 let line_char_count = line_text.chars().count();
435 let clamped_index = char_index.min(line_char_count);
436
437 let offset_in_line = line_text
438 .char_indices()
439 .nth(clamped_index)
440 .map_or(line_text.len(), |(i, _)| i);
441
442 line_start_byte + offset_in_line
443 }
444
445 fn get_cursor_x_for_offset(
446 &self,
447 text: &crate::text::AnnotatedString,
448 style: &TextStyle,
449 offset: usize,
450 ) -> f32 {
451 let (char_width, _) = Self::get_metrics(style);
452
453 let clamped_offset = offset.min(text.text.len());
454 let char_count = text.text[..clamped_offset].chars().count();
455 char_count as f32 * char_width
456 }
457
458 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
459 let (char_width, line_height) = Self::get_metrics(style);
460 TextLayoutResult::monospaced(&text.text, char_width, line_height)
461 }
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
465struct TextBaseCacheKey {
466 text_hash: u64,
467 style_hash: u64,
468}
469
470#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
471struct TextOptionsCacheKey {
472 base: TextBaseCacheKey,
473 options: TextLayoutOptions,
474 max_width_bits: Option<u32>,
475}
476
477#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
478struct TextPreparedCacheKey {
479 base: TextOptionsCacheKey,
480 visual_hash: u64,
481}
482
483struct BoundedTextCache<K, V> {
484 capacity: usize,
485 entries: cranpose_core::collections::map::HashMap<K, V>,
486 order: VecDeque<K>,
487}
488
489impl<K, V> BoundedTextCache<K, V>
490where
491 K: Clone + Eq + Hash,
492 V: Clone,
493{
494 fn new(capacity: usize) -> Self {
495 Self {
496 capacity,
497 entries: cranpose_core::collections::map::HashMap::default(),
498 order: VecDeque::new(),
499 }
500 }
501
502 fn clear(&mut self) {
503 self.entries.clear();
504 self.order.clear();
505 }
506
507 fn get(&self, key: &K) -> Option<V> {
508 self.entries.get(key).cloned()
509 }
510
511 fn insert(&mut self, key: K, value: V) {
512 match self.entries.entry(key.clone()) {
513 Entry::Occupied(mut entry) => {
514 entry.insert(value);
515 return;
516 }
517 Entry::Vacant(_) => {}
518 }
519 if self.entries.len() == self.capacity {
520 while let Some(evicted) = self.order.pop_front() {
521 if self.entries.remove(&evicted).is_some() {
522 break;
523 }
524 }
525 }
526 self.order.push_back(key.clone());
527 self.entries.insert(key, value);
528 }
529}
530
531pub(crate) struct TextService {
532 generation: Cell<u64>,
533 measurer: RefCell<Rc<dyn TextMeasurer>>,
534 metrics_cache: RefCell<BoundedTextCache<TextBaseCacheKey, TextMetrics>>,
535 options_metrics_cache: RefCell<BoundedTextCache<TextOptionsCacheKey, TextMetrics>>,
536 prepared_cache: RefCell<BoundedTextCache<TextPreparedCacheKey, Rc<PreparedTextLayout>>>,
537 layout_cache: RefCell<BoundedTextCache<TextBaseCacheKey, TextLayoutResult>>,
538}
539
540impl TextService {
541 pub(crate) fn new() -> Self {
542 Self::from_measurer(Rc::new(MonospacedTextMeasurer))
543 }
544
545 pub(crate) fn from_measurer(measurer: Rc<dyn TextMeasurer>) -> Self {
546 Self {
547 generation: Cell::new(1),
548 measurer: RefCell::new(measurer),
549 metrics_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
550 options_metrics_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
551 prepared_cache: RefCell::new(BoundedTextCache::new(TEXT_PREPARED_CACHE_CAPACITY)),
552 layout_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
553 }
554 }
555
556 pub(crate) fn set_measurer(&self, measurer: Rc<dyn TextMeasurer>) {
557 *self.measurer.borrow_mut() = measurer;
558 self.clear_caches();
559 }
560
561 pub(crate) fn generation(&self) -> u64 {
562 self.generation.get()
563 }
564
565 pub(crate) fn current_measurer(&self) -> Rc<dyn TextMeasurer> {
566 Rc::clone(&self.measurer.borrow())
567 }
568
569 pub(crate) fn with_measurer<R>(&self, f: impl FnOnce(&dyn TextMeasurer) -> R) -> R {
570 let measurer = self.current_measurer();
571 f(&*measurer)
572 }
573
574 pub(crate) fn measure(
575 &self,
576 node_id: Option<NodeId>,
577 text: &crate::text::AnnotatedString,
578 style: &TextStyle,
579 ) -> TextMetrics {
580 let key = text_base_cache_key(text, style);
581 if let Some(metrics) = self.metrics_cache.borrow().get(&key) {
582 return metrics;
583 }
584 let metrics = self.with_measurer(|m| m.measure_for_node(node_id, text, style));
585 self.metrics_cache.borrow_mut().insert(key, metrics);
586 metrics
587 }
588
589 pub(crate) fn measure_with_options(
590 &self,
591 node_id: Option<NodeId>,
592 text: &crate::text::AnnotatedString,
593 style: &TextStyle,
594 options: TextLayoutOptions,
595 max_width: Option<f32>,
596 ) -> TextMetrics {
597 let key = text_options_cache_key(text, style, options.normalized(), max_width);
598 if let Some(metrics) = self.options_metrics_cache.borrow().get(&key) {
599 return metrics;
600 }
601 let metrics = self.with_measurer(|m| {
602 m.measure_with_options_for_node(node_id, text, style, options.normalized(), max_width)
603 });
604 self.options_metrics_cache.borrow_mut().insert(key, metrics);
605 metrics
606 }
607
608 pub(crate) fn prepare_with_options(
612 &self,
613 node_id: Option<NodeId>,
614 text: &crate::text::AnnotatedString,
615 style: &TextStyle,
616 options: TextLayoutOptions,
617 max_width: Option<f32>,
618 ) -> Rc<PreparedTextLayout> {
619 let metrics_key = text_options_cache_key(text, style, options.normalized(), max_width);
620 let key = TextPreparedCacheKey {
621 base: metrics_key,
622 visual_hash: style.render_hash(),
623 };
624 if let Some(prepared) = self.prepared_cache.borrow().get(&key) {
625 return prepared;
626 }
627 let prepared = Rc::new(self.with_measurer(|m| {
628 m.prepare_with_options_for_node(node_id, text, style, options.normalized(), max_width)
629 }));
630 self.prepared_cache
631 .borrow_mut()
632 .insert(key, Rc::clone(&prepared));
633 self.options_metrics_cache
634 .borrow_mut()
635 .insert(metrics_key, prepared.metrics);
636 prepared
637 }
638
639 pub(crate) fn layout(
640 &self,
641 text: &crate::text::AnnotatedString,
642 style: &TextStyle,
643 ) -> TextLayoutResult {
644 let key = text_base_cache_key(text, style);
645 if let Some(layout) = self.layout_cache.borrow().get(&key) {
646 return layout;
647 }
648 let layout = self.with_measurer(|m| m.layout(text, style));
649 self.layout_cache.borrow_mut().insert(key, layout.clone());
650 layout
651 }
652
653 fn clear_caches(&self) {
654 self.generation
655 .set(self.generation.get().wrapping_add(1).max(1));
656 self.metrics_cache.borrow_mut().clear();
657 self.options_metrics_cache.borrow_mut().clear();
658 self.prepared_cache.borrow_mut().clear();
659 self.layout_cache.borrow_mut().clear();
660 }
661}
662
663fn text_base_cache_key(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextBaseCacheKey {
664 TextBaseCacheKey {
665 text_hash: text.render_hash(),
666 style_hash: style.measurement_hash(),
667 }
668}
669
670fn text_options_cache_key(
671 text: &crate::text::AnnotatedString,
672 style: &TextStyle,
673 options: TextLayoutOptions,
674 max_width: Option<f32>,
675) -> TextOptionsCacheKey {
676 TextOptionsCacheKey {
677 base: text_base_cache_key(text, style),
678 options: options.normalized(),
679 max_width_bits: normalize_max_width(max_width).map(f32::to_bits),
680 }
681}
682
683pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
684 crate::render_state::set_current_text_measurer(Rc::new(measurer));
685}
686
687pub(crate) fn current_text_generation() -> u64 {
688 crate::render_state::with_text_service(TextService::generation)
689}
690
691pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
692 with_system_font_scale(text, style, |text, style| {
693 crate::render_state::with_text_service(|service| service.measure(None, text, style))
694 })
695}
696
697pub(crate) fn measure_resolved_text(
698 text: &crate::text::AnnotatedString,
699 style: &TextStyle,
700) -> TextMetrics {
701 crate::render_state::with_text_service(|service| service.measure(None, text, style))
702}
703
704pub(crate) fn resolved_first_baseline(style: &TextStyle) -> Option<f32> {
705 crate::render_state::with_text_service(|service| {
706 service.with_measurer(|m| m.first_baseline(style))
707 })
708}
709
710pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
711 crate::render_state::current_app_context()?;
712 crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(style)))
713}
714
715pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
719 let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
720 crate::render_state::with_text_service(|service| {
721 service.with_measurer(|m| m.glyph_line_box(&style))
722 })
723 .map_or((0.0, line_height), |(off, h)| {
724 (off.min(line_height), h.min(line_height))
725 })
726}
727
728pub fn text_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
732 let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
733 crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(&style)))
734}
735
736pub fn first_baseline(style: &TextStyle) -> Option<f32> {
740 let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
741 crate::render_state::with_text_service(|service| {
742 service.with_measurer(|m| m.first_baseline(&style))
743 })
744}
745
746pub fn measure_text_for_node(
747 node_id: Option<NodeId>,
748 text: &crate::text::AnnotatedString,
749 style: &TextStyle,
750) -> TextMetrics {
751 with_system_font_scale(text, style, |text, style| {
752 crate::render_state::with_text_service(|service| service.measure(node_id, text, style))
753 })
754}
755
756pub fn measure_text_with_options(
757 text: &crate::text::AnnotatedString,
758 style: &TextStyle,
759 options: TextLayoutOptions,
760 max_width: Option<f32>,
761) -> TextMetrics {
762 with_system_font_scale(text, style, |text, style| {
763 crate::render_state::with_text_service(|service| {
764 service.measure_with_options(None, text, style, options.normalized(), max_width)
765 })
766 })
767}
768
769pub fn measure_text_with_options_for_node(
770 node_id: Option<NodeId>,
771 text: &crate::text::AnnotatedString,
772 style: &TextStyle,
773 options: TextLayoutOptions,
774 max_width: Option<f32>,
775) -> TextMetrics {
776 with_system_font_scale(text, style, |text, style| {
777 crate::render_state::with_text_service(|service| {
778 service.measure_with_options(node_id, text, style, options.normalized(), max_width)
779 })
780 })
781}
782
783pub fn prepare_text_layout(
784 text: &crate::text::AnnotatedString,
785 style: &TextStyle,
786 options: TextLayoutOptions,
787 max_width: Option<f32>,
788) -> PreparedTextLayout {
789 Rc::unwrap_or_clone(prepare_text_layout_for_node(
790 None, text, style, options, max_width,
791 ))
792}
793
794pub fn prepare_text_layout_for_node(
795 node_id: Option<NodeId>,
796 text: &crate::text::AnnotatedString,
797 style: &TextStyle,
798 options: TextLayoutOptions,
799 max_width: Option<f32>,
800) -> Rc<PreparedTextLayout> {
801 with_system_font_scale(text, style, |text, style| {
802 crate::render_state::with_text_service(|service| {
803 service.prepare_with_options(node_id, text, style, options.normalized(), max_width)
804 })
805 })
806}
807
808pub fn get_offset_for_position(
809 text: &crate::text::AnnotatedString,
810 style: &TextStyle,
811 x: f32,
812 y: f32,
813) -> usize {
814 with_system_font_scale(text, style, |text, style| {
815 crate::render_state::with_text_measurer(|m| m.get_offset_for_position(text, style, x, y))
816 })
817}
818
819pub fn offset_for_position_wrapped(
833 text: &str,
834 style: &TextStyle,
835 node_id: Option<NodeId>,
836 wrap_width: Option<f32>,
837 line_height: f32,
838 x: f32,
839 y: f32,
840) -> usize {
841 if text.is_empty() {
842 return 0;
843 }
844 let annotated = crate::text::AnnotatedString::from(text);
845 let line_ranges = wrapped_line_ranges(
846 node_id,
847 &annotated,
848 style,
849 TextLayoutOptions::default(),
850 wrap_width,
851 );
852 if line_ranges.is_empty() {
853 return 0;
854 }
855 let line_idx = if line_height > 0.0 {
856 (y / line_height).floor().max(0.0) as usize
857 } else {
858 0
859 }
860 .min(line_ranges.len() - 1);
861 let range = &line_ranges[line_idx];
862 let line = &text[range.start..range.end];
863 let within = get_offset_for_position(&crate::text::AnnotatedString::from(line), style, x, 0.0);
864 range.start + within.min(line.len())
865}
866
867pub fn get_cursor_x_for_offset(
868 text: &crate::text::AnnotatedString,
869 style: &TextStyle,
870 offset: usize,
871) -> f32 {
872 with_system_font_scale(text, style, |text, style| {
873 crate::render_state::with_text_measurer(|m| m.get_cursor_x_for_offset(text, style, offset))
874 })
875}
876
877pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
878 with_system_font_scale(text, style, |text, style| {
879 crate::render_state::with_text_service(|service| service.layout(text, style))
880 })
881}
882
883pub fn wrapped_line_ranges(
896 node_id: Option<NodeId>,
897 text: &crate::text::AnnotatedString,
898 style: &TextStyle,
899 options: TextLayoutOptions,
900 max_width: Option<f32>,
901) -> Vec<Range<usize>> {
902 with_system_font_scale(text, style, |text, style| {
903 crate::render_state::with_text_measurer(|m| {
904 wrapped_line_ranges_with_measurer(m, node_id, text, style, options, max_width)
905 })
906 })
907}
908
909fn wrapped_line_ranges_with_measurer<M: TextMeasurer + ?Sized>(
910 measurer: &M,
911 _node_id: Option<NodeId>,
912 text: &crate::text::AnnotatedString,
913 style: &TextStyle,
914 options: TextLayoutOptions,
915 max_width: Option<f32>,
916) -> Vec<Range<usize>> {
917 let opts = options.normalized();
918 let max_width = normalize_max_width(max_width);
919 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
920 .then_some(max_width)
921 .flatten();
922 let line_break_mode = style
923 .paragraph_style
924 .line_break
925 .take_or_else(|| LineBreak::Simple);
926 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
927
928 let line_ranges = split_line_ranges(text.text.as_str());
929 let Some(width_limit) = wrap_width else {
930 return line_ranges;
931 };
932 let mut ranges = Vec::with_capacity(line_ranges.len());
933 for line_range in line_ranges {
934 for display_line in wrap_line_to_width(
935 measurer,
936 text,
937 line_range,
938 style,
939 width_limit,
940 line_break_mode,
941 hyphens_mode,
942 ) {
943 ranges.push(display_line.source_range.clone());
944 }
945 }
946 ranges
947}
948
949fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
950 measurer: &M,
951 text: &crate::text::AnnotatedString,
952 style: &TextStyle,
953 options: TextLayoutOptions,
954 max_width: Option<f32>,
955) -> PreparedTextLayout {
956 prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
957}
958
959pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
960 measurer: &M,
961 node_id: Option<NodeId>,
962 text: &crate::text::AnnotatedString,
963 style: &TextStyle,
964 options: TextLayoutOptions,
965 max_width: Option<f32>,
966) -> PreparedTextLayout {
967 let telemetry = text_layout_telemetry_enabled();
968 let total_start = telemetry.then(Instant::now);
969 let opts = options.normalized();
970 let max_width = normalize_max_width(max_width);
971 if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
972 return prepare_scale_down_text_layout(
973 measurer,
974 node_id,
975 text,
976 style,
977 opts,
978 max_width,
979 min_font_size_sp,
980 );
981 }
982
983 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
984 .then_some(max_width)
985 .flatten();
986 let line_break_mode = style
987 .paragraph_style
988 .line_break
989 .take_or_else(|| LineBreak::Simple);
990 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
991
992 let wrap_start = telemetry.then(Instant::now);
993 let line_ranges = split_line_ranges(text.text.as_str());
994 let source_line_count = line_ranges.len();
995 let mut visible_lines: Vec<DisplayLine>;
996 if let Some(width_limit) = wrap_width {
997 visible_lines = Vec::with_capacity(line_ranges.len());
998 for line_range in line_ranges {
999 let wrapped_lines = wrap_line_to_width(
1000 measurer,
1001 text,
1002 line_range,
1003 style,
1004 width_limit,
1005 line_break_mode,
1006 hyphens_mode,
1007 );
1008 visible_lines.extend(wrapped_lines);
1009 }
1010 } else {
1011 visible_lines = line_ranges
1012 .into_iter()
1013 .map(DisplayLine::from_source_range)
1014 .collect();
1015 }
1016 let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1017
1018 let overflow_start = telemetry.then(Instant::now);
1019 let did_overflow = apply_overflow(
1020 measurer,
1021 node_id,
1022 text,
1023 style,
1024 opts,
1025 max_width,
1026 &mut visible_lines,
1027 );
1028 let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1029
1030 let build_start = telemetry.then(Instant::now);
1031 let display_annotated = build_display_annotated(text, &visible_lines);
1032 debug_assert_eq!(
1033 display_annotated.text,
1034 join_display_line_text(text, &visible_lines)
1035 );
1036 let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1037
1038 let metrics_start = telemetry.then(Instant::now);
1039 let line_height = measurer.line_height_for_node(node_id, text, style).max(0.0);
1040 let display_line_count = visible_lines.len().max(1);
1041 let layout_line_count = display_line_count.max(opts.min_lines);
1042
1043 let measured_width = if visible_lines.is_empty() {
1044 0.0
1045 } else {
1046 visible_lines
1047 .iter()
1048 .map(|line| line.measure_width(measurer, node_id, text, style))
1049 .fold(0.0_f32, f32::max)
1050 };
1051 let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1052 let width = if opts.overflow == TextOverflow::Visible {
1053 measured_width
1054 } else if let Some(width_limit) = max_width {
1055 measured_width.min(width_limit)
1056 } else {
1057 measured_width
1058 };
1059
1060 let edges = measurer
1061 .line_box(style)
1062 .unwrap_or_else(|| crate::text::LineBox::untrimmed(line_height, 0.0));
1063 let prepared = PreparedTextLayout {
1064 text: Rc::new(display_annotated),
1065 visual_style: style.clone(),
1066 metrics: TextMetrics {
1067 width,
1068 height: (layout_line_count as f32 * line_height - edges.trim_top - edges.trim_bottom)
1069 .max(0.0),
1070 line_height,
1071 line_count: layout_line_count,
1072 },
1073 did_overflow,
1074 };
1075
1076 if let Some(start) = total_start {
1077 eprintln!(
1078 "[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}",
1079 text.text.len(),
1080 text.span_styles.len(),
1081 source_line_count,
1082 display_line_count,
1083 wrap_width.is_some(),
1084 max_width,
1085 wrap_ms.unwrap_or(0.0),
1086 overflow_ms.unwrap_or(0.0),
1087 build_ms.unwrap_or(0.0),
1088 metrics_ms.unwrap_or(0.0),
1089 start.elapsed().as_secs_f64() * 1000.0,
1090 );
1091 }
1092
1093 prepared
1094}
1095
1096fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1097 measurer: &M,
1098 node_id: Option<NodeId>,
1099 text: &crate::text::AnnotatedString,
1100 style: &TextStyle,
1101 options: TextLayoutOptions,
1102 max_width: Option<f32>,
1103 min_font_size_sp: f32,
1104) -> PreparedTextLayout {
1105 let clipped_options = TextLayoutOptions {
1106 overflow: TextOverflow::Clip,
1107 ..options
1108 }
1109 .normalized();
1110
1111 let full_size = prepare_scaled_text_layout(
1112 measurer,
1113 node_id,
1114 text,
1115 style,
1116 clipped_options,
1117 max_width,
1118 FontScaleCurve::linear(1.0),
1119 );
1120 let Some(width_limit) = max_width else {
1121 return full_size;
1122 };
1123 if !full_size.did_overflow {
1124 return full_size;
1125 }
1126
1127 let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1128 if !base_font_size.is_finite() || base_font_size <= 0.0 {
1129 return full_size;
1130 }
1131 let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1132 if min_scale >= 1.0 {
1133 return full_size;
1134 }
1135
1136 let min_size = prepare_scaled_text_layout(
1137 measurer,
1138 node_id,
1139 text,
1140 style,
1141 clipped_options,
1142 Some(width_limit),
1143 FontScaleCurve::linear(min_scale),
1144 );
1145 if min_size.did_overflow {
1146 return min_size;
1147 }
1148
1149 let mut low = min_scale;
1150 let mut high = 1.0;
1151 let mut best = min_size;
1152 for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1153 let mid = (low + high) * 0.5;
1154 let candidate = prepare_scaled_text_layout(
1155 measurer,
1156 node_id,
1157 text,
1158 style,
1159 clipped_options,
1160 Some(width_limit),
1161 FontScaleCurve::linear(mid),
1162 );
1163 if candidate.did_overflow {
1164 high = mid;
1165 } else {
1166 low = mid;
1167 best = candidate;
1168 }
1169 }
1170
1171 best
1172}
1173
1174fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1175 measurer: &M,
1176 node_id: Option<NodeId>,
1177 text: &crate::text::AnnotatedString,
1178 style: &TextStyle,
1179 options: TextLayoutOptions,
1180 max_width: Option<f32>,
1181 shrink: FontScaleCurve,
1182) -> PreparedTextLayout {
1183 let visual_style = scale_text_style_font_sizes(style, shrink);
1184 let visual_text = scale_annotated_font_sizes(text, shrink);
1185 prepare_text_layout_with_measurer_for_node(
1186 measurer,
1187 node_id,
1188 visual_text.as_ref(),
1189 &visual_style,
1190 options,
1191 max_width,
1192 )
1193}
1194
1195fn scale_annotated_font_sizes(
1196 text: &crate::text::AnnotatedString,
1197 curve: FontScaleCurve,
1198) -> Cow<'_, crate::text::AnnotatedString> {
1199 if curve.is_identity() || !annotated_text_needs_scaling(text) {
1200 return Cow::Borrowed(text);
1201 }
1202
1203 let mut scaled = text.clone();
1204 for span in &mut scaled.span_styles {
1205 span.item = scale_span_style_font_sizes(&span.item, curve, None);
1206 }
1207 Cow::Owned(scaled)
1208}
1209
1210fn scale_text_style_font_sizes(style: &TextStyle, curve: FontScaleCurve) -> TextStyle {
1211 if curve.is_identity() {
1212 return style.clone();
1213 }
1214
1215 let mut scaled = style.clone();
1216 scaled.span_style =
1217 scale_span_style_font_sizes(&style.span_style, curve, Some(DEFAULT_FONT_SIZE_SP));
1218 scaled.paragraph_style.line_height =
1219 scale_text_unit_sp(scaled.paragraph_style.line_height, curve);
1220 if let Some(mut indent) = scaled.paragraph_style.text_indent {
1221 indent.first_line = scale_text_unit_sp(indent.first_line, curve);
1222 indent.rest_line = scale_text_unit_sp(indent.rest_line, curve);
1223 scaled.paragraph_style.text_indent = Some(indent);
1224 }
1225 scaled
1226}
1227
1228fn with_system_font_scale<R>(
1229 text: &crate::text::AnnotatedString,
1230 style: &TextStyle,
1231 block: impl FnOnce(&crate::text::AnnotatedString, &TextStyle) -> R,
1232) -> R {
1233 let curve = crate::current_font_scale_curve();
1234 let visual_style = scale_text_style_font_sizes(style, curve);
1235 let visual_text = scale_annotated_font_sizes(text, curve);
1236 block(visual_text.as_ref(), &visual_style)
1237}
1238
1239fn scale_span_style_font_sizes(
1240 style: &crate::text::SpanStyle,
1241 curve: FontScaleCurve,
1242 default_font_size_sp: Option<f32>,
1243) -> crate::text::SpanStyle {
1244 let factor = curve.scale();
1245 let mut scaled = style.clone();
1246 scaled.font_size = match (style.font_size, default_font_size_sp) {
1247 (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1248 crate::text::TextUnit::Sp(curve.sp_to_dp(default_size))
1249 }
1250 (unit, Some(_)) => scale_text_unit_sp_and_em(unit, curve),
1251 (unit, None) => scale_text_unit_sp(unit, curve),
1252 };
1253 scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, curve);
1254 if let Some(mut shadow) = scaled.shadow {
1255 shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1256 shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1257 shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1258 scaled.shadow = Some(shadow);
1259 }
1260 if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1261 scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1262 width: width * factor,
1263 });
1264 }
1265 scaled
1266}
1267
1268fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1269 text.span_styles
1270 .iter()
1271 .any(|span| span_style_needs_scaling(&span.item))
1272}
1273
1274fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1275 matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1276 || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1277 || matches!(
1278 style.draw_style,
1279 Some(crate::text::TextDrawStyle::Stroke { .. })
1280 )
1281 || style.shadow.is_some()
1282}
1283
1284fn scale_text_unit_sp(unit: crate::text::TextUnit, curve: FontScaleCurve) -> crate::text::TextUnit {
1285 match unit {
1286 crate::text::TextUnit::Sp(value) if value.is_finite() => {
1287 crate::text::TextUnit::Sp(curve.sp_to_dp(value))
1288 }
1289 other => other,
1290 }
1291}
1292
1293fn scale_text_unit_sp_and_em(
1294 unit: crate::text::TextUnit,
1295 curve: FontScaleCurve,
1296) -> crate::text::TextUnit {
1297 match unit {
1298 crate::text::TextUnit::Sp(_) => scale_text_unit_sp(unit, curve),
1299 crate::text::TextUnit::Em(value) if value.is_finite() => {
1300 crate::text::TextUnit::Em(value * curve.scale())
1301 }
1302 other => other,
1303 }
1304}
1305
1306fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1307 if value.is_finite() {
1308 value * factor
1309 } else {
1310 value
1311 }
1312}
1313
1314#[derive(Clone, Debug)]
1315enum DisplayLineText {
1316 Source,
1317 Ellipsized(crate::text::AnnotatedString),
1318}
1319
1320#[derive(Clone, Debug)]
1321struct DisplayLine {
1322 source_range: Range<usize>,
1323 text: DisplayLineText,
1324 measured_width: Option<f32>,
1325}
1326
1327impl DisplayLine {
1328 fn from_source_range(source_range: Range<usize>) -> Self {
1329 Self {
1330 source_range,
1331 text: DisplayLineText::Source,
1332 measured_width: None,
1333 }
1334 }
1335
1336 fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1337 Self {
1338 source_range,
1339 text: DisplayLineText::Source,
1340 measured_width: measured_width
1341 .is_finite()
1342 .then_some(measured_width.max(0.0)),
1343 }
1344 }
1345
1346 fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1347 match &self.text {
1348 DisplayLineText::Source => &source.text[self.source_range.clone()],
1349 DisplayLineText::Ellipsized(annotated) => annotated.text.as_str(),
1350 }
1351 }
1352
1353 fn measure_width<M: TextMeasurer + ?Sized>(
1354 &self,
1355 measurer: &M,
1356 node_id: Option<NodeId>,
1357 source: &crate::text::AnnotatedString,
1358 style: &TextStyle,
1359 ) -> f32 {
1360 self.measured_width.unwrap_or_else(|| match &self.text {
1361 DisplayLineText::Source => {
1362 measurer
1363 .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1364 .width
1365 }
1366 DisplayLineText::Ellipsized(annotated) => {
1367 measurer.measure_for_node(node_id, annotated, style).width
1368 }
1369 })
1370 }
1371
1372 fn extend_to_paragraph_end(&mut self, source: &crate::text::AnnotatedString) {
1373 let start = self.source_range.start;
1374 let end = source.text[start..]
1375 .find('\n')
1376 .map_or(source.text.len(), |offset| start + offset);
1377 self.source_range = start..end;
1378 self.text = DisplayLineText::Source;
1379 self.measured_width = None;
1380 }
1381
1382 fn ellipsize<M: TextMeasurer + ?Sized>(
1383 &mut self,
1384 measurer: &M,
1385 node_id: Option<NodeId>,
1386 source: &crate::text::AnnotatedString,
1387 style: &TextStyle,
1388 max_width: Option<f32>,
1389 placement: EllipsisPlacement,
1390 ) {
1391 *self = fit_ellipsis(
1392 measurer,
1393 node_id,
1394 source,
1395 self.source_range.clone(),
1396 style,
1397 max_width,
1398 placement,
1399 );
1400 }
1401}
1402
1403fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1404 if text.is_empty() {
1405 return single_line_range(0..0);
1406 }
1407
1408 let mut ranges = Vec::new();
1409 let mut start = 0usize;
1410 for (idx, ch) in text.char_indices() {
1411 if ch == '\n' {
1412 ranges.push(start..idx);
1413 start = idx + ch.len_utf8();
1414 }
1415 }
1416 ranges.push(start..text.len());
1417 ranges
1418}
1419
1420fn build_display_annotated(
1421 source: &crate::text::AnnotatedString,
1422 lines: &[DisplayLine],
1423) -> crate::text::AnnotatedString {
1424 if lines.is_empty() {
1425 return crate::text::AnnotatedString::from("");
1426 }
1427
1428 let mut builder = crate::text::AnnotatedString::builder();
1429 for (idx, line) in lines.iter().enumerate() {
1430 builder = match &line.text {
1431 DisplayLineText::Source => {
1432 builder.append_annotated_subsequence(source, line.source_range.clone())
1433 }
1434 DisplayLineText::Ellipsized(annotated) => builder.append_annotated(annotated),
1435 };
1436 if idx + 1 < lines.len() {
1437 builder = builder.append("\n");
1438 }
1439 }
1440 builder.to_annotated_string()
1441}
1442
1443fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1444 let mut text = String::new();
1445 for (idx, line) in lines.iter().enumerate() {
1446 text.push_str(line.display_text(source));
1447 if idx + 1 < lines.len() {
1448 text.push('\n');
1449 }
1450 }
1451 text
1452}
1453
1454fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1455 while end > start {
1456 let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1457 break;
1458 };
1459 if ch.is_whitespace() {
1460 end = start + idx;
1461 } else {
1462 break;
1463 }
1464 }
1465 end
1466}
1467
1468#[derive(Clone, Copy, Debug, PartialEq)]
1471pub(crate) enum PreparedWidths {
1472 Exact(Option<u32>),
1474 AtLeast(f32),
1477}
1478
1479impl PreparedWidths {
1480 pub(crate) fn of(
1482 text: &crate::text::AnnotatedString,
1483 options: TextLayoutOptions,
1484 max_width: Option<f32>,
1485 prepared: &PreparedTextLayout,
1486 ) -> Self {
1487 let max_width = normalize_max_width(max_width);
1488 let exact = Self::Exact(max_width.map(f32::to_bits));
1489 let wrapped = prepared.text.text.matches('\n').count() != text.text.matches('\n').count();
1490 let trailing_space = text
1493 .text
1494 .split('\n')
1495 .any(|line| line.ends_with(char::is_whitespace));
1496 if options
1497 .normalized()
1498 .overflow
1499 .scale_down_min_font_size_sp()
1500 .is_some()
1501 || prepared.did_overflow
1502 || wrapped
1503 || trailing_space
1504 {
1505 return exact;
1506 }
1507 match max_width {
1508 Some(width) if prepared.metrics.width >= width => exact,
1509 _ => Self::AtLeast(prepared.metrics.width),
1510 }
1511 }
1512
1513 pub(crate) fn hold(self, max_width: Option<f32>) -> bool {
1515 let max_width = normalize_max_width(max_width);
1516 match self {
1517 Self::Exact(bits) => max_width.map(f32::to_bits) == bits,
1518 Self::AtLeast(min) => max_width.is_none_or(|width| width >= min),
1519 }
1520 }
1521}
1522
1523fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1524 match max_width {
1525 Some(width) if width.is_finite() && width > 0.0 => Some(width),
1526 _ => None,
1527 }
1528}
1529
1530fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1531 (base_start + relative.start)..(base_start + relative.end)
1532}
1533
1534fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1535 boundaries
1536 .binary_search(&byte_offset)
1537 .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1538}
1539
1540fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1541 std::iter::once(range).collect()
1542}
1543
1544struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1545 measurer: &'a M,
1546 text: &'a crate::text::AnnotatedString,
1547 style: &'a TextStyle,
1548 line_start: usize,
1549 prefix_widths: Option<TextLinePrefixWidths>,
1550}
1551
1552impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1553 fn new(
1554 measurer: &'a M,
1555 text: &'a crate::text::AnnotatedString,
1556 line_range: &Range<usize>,
1557 style: &'a TextStyle,
1558 boundary_count: usize,
1559 ) -> Self {
1560 let expected_chars = boundary_count.saturating_sub(1);
1561 let prefix_widths = measurer
1562 .measure_line_prefix_widths(text, line_range.clone(), style)
1563 .filter(|widths| widths.char_count() == expected_chars);
1564 Self {
1565 measurer,
1566 text,
1567 style,
1568 line_start: line_range.start,
1569 prefix_widths,
1570 }
1571 }
1572
1573 fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1574 if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1575 return width;
1576 }
1577 let segment_range =
1578 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1579 self.measurer
1580 .measure_subsequence(self.text, segment_range, self.style)
1581 .width
1582 }
1583
1584 fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1585 if let Some(prefix_widths) = &self.prefix_widths
1586 && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1587 {
1588 return Some(width);
1589 }
1590 None
1591 }
1592
1593 fn display_line_for_char_range(
1594 &self,
1595 boundaries: &[usize],
1596 start_idx: usize,
1597 end_idx: usize,
1598 ) -> DisplayLine {
1599 let source_range =
1600 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1601 let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1602 DisplayLine::from_measured_source_range(source_range, measured_width)
1603 }
1604}
1605
1606fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1607 measurer: &M,
1608 text: &crate::text::AnnotatedString,
1609 line_range: Range<usize>,
1610 style: &TextStyle,
1611 max_width: f32,
1612 line_break: LineBreak,
1613 hyphens: Hyphens,
1614) -> Vec<DisplayLine> {
1615 let line_text = &text.text[line_range.clone()];
1616 if line_text.is_empty() {
1617 return vec![DisplayLine::from_source_range(
1618 line_range.start..line_range.start,
1619 )];
1620 }
1621
1622 if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
1623 && measured_width <= max_width + WRAP_EPSILON
1624 {
1625 return vec![DisplayLine::from_measured_source_range(
1626 line_range,
1627 measured_width,
1628 )];
1629 }
1630
1631 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1632 && line_text.chars().any(char::is_whitespace)
1633 && let Some(balanced) = wrap_line_with_word_balance(
1634 measurer,
1635 text,
1636 line_range.clone(),
1637 style,
1638 max_width,
1639 line_break,
1640 )
1641 {
1642 return balanced;
1643 }
1644
1645 wrap_line_greedy(
1646 measurer, text, line_range, style, max_width, line_break, hyphens,
1647 )
1648}
1649
1650fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1651 measurer: &M,
1652 text: &crate::text::AnnotatedString,
1653 line_range: Range<usize>,
1654 style: &TextStyle,
1655 max_width: f32,
1656 line_break: LineBreak,
1657 hyphens: Hyphens,
1658) -> Vec<DisplayLine> {
1659 let line_text = &text.text[line_range.clone()];
1660 let boundaries = char_boundaries(line_text);
1661 let measure_context =
1662 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1663 if let Some(measured_width) =
1664 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1665 && measured_width <= max_width + WRAP_EPSILON
1666 {
1667 return vec![DisplayLine::from_measured_source_range(
1668 line_range,
1669 measured_width,
1670 )];
1671 }
1672 let mut wrapped = Vec::new();
1673 let mut start_idx = 0usize;
1674
1675 while start_idx < boundaries.len() - 1 {
1676 let mut low = start_idx + 1;
1677 let mut high = boundaries.len() - 1;
1678 let mut best = start_idx + 1;
1679
1680 while low <= high {
1681 let mid = (low + high) / 2;
1682 let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1683 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1684 best = mid;
1685 low = mid + 1;
1686 } else {
1687 if mid == 0 {
1688 break;
1689 }
1690 high = mid - 1;
1691 }
1692 }
1693
1694 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1695 let mut effective_wrap_idx = wrap_idx;
1696 let can_hyphenate = hyphens == Hyphens::Auto
1697 && wrap_idx == best
1698 && best < boundaries.len() - 1
1699 && is_break_inside_word(line_text, &boundaries, wrap_idx);
1700 if can_hyphenate {
1701 effective_wrap_idx = resolve_auto_hyphen_break(
1702 measurer,
1703 line_text,
1704 style,
1705 &boundaries,
1706 start_idx,
1707 wrap_idx,
1708 );
1709 }
1710
1711 let broke_at_word_boundary = effective_wrap_idx > start_idx
1712 && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
1713 .chars()
1714 .all(char::is_whitespace);
1715 let segment_start = boundaries[start_idx];
1716 let mut segment_end = boundaries[effective_wrap_idx];
1717 if wrap_idx != best || broke_at_word_boundary {
1718 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1719 }
1720 let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1721 wrapped.push(measure_context.display_line_for_char_range(
1722 &boundaries,
1723 start_idx,
1724 segment_end_idx,
1725 ));
1726
1727 start_idx = if wrap_idx != best || broke_at_word_boundary {
1728 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1729 } else {
1730 effective_wrap_idx
1731 };
1732 }
1733
1734 if wrapped.is_empty() {
1735 wrapped.push(DisplayLine::from_source_range(
1736 line_range.start..line_range.start,
1737 ));
1738 }
1739
1740 wrapped
1741}
1742
1743fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1744 measurer: &M,
1745 text: &crate::text::AnnotatedString,
1746 line_range: Range<usize>,
1747 style: &TextStyle,
1748 max_width: f32,
1749 line_break: LineBreak,
1750) -> Option<Vec<DisplayLine>> {
1751 let line_text = &text.text[line_range.clone()];
1752 let boundaries = char_boundaries(line_text);
1753 let measure_context =
1754 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1755 if let Some(measured_width) =
1756 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1757 && measured_width <= max_width + WRAP_EPSILON
1758 {
1759 return Some(vec![DisplayLine::from_measured_source_range(
1760 line_range,
1761 measured_width,
1762 )]);
1763 }
1764 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1765 if breakpoints.len() <= 2 {
1766 return None;
1767 }
1768
1769 let node_count = breakpoints.len();
1770 let mut best_cost = vec![f32::INFINITY; node_count];
1771 let mut next_index = vec![None; node_count];
1772 best_cost[node_count - 1] = 0.0;
1773
1774 for start in (0..node_count - 1).rev() {
1775 for end in start + 1..node_count {
1776 let start_byte = boundaries[breakpoints[start]];
1777 let end_byte = boundaries[breakpoints[end]];
1778 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1779 if trimmed_end <= start_byte {
1780 continue;
1781 }
1782 let segment_start_idx = breakpoints[start];
1783 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1784 let segment_width =
1785 measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1786 if segment_width > max_width + WRAP_EPSILON {
1787 continue;
1788 }
1789 if !best_cost[end].is_finite() {
1790 continue;
1791 }
1792 let slack = (max_width - segment_width).max(0.0);
1793 let is_last = end == node_count - 1;
1794 let segment_cost = match line_break {
1795 LineBreak::Heading => slack * slack,
1796 LineBreak::Paragraph => {
1797 if is_last {
1798 slack * slack * 0.16
1799 } else {
1800 slack * slack
1801 }
1802 }
1803 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1804 };
1805 let candidate = segment_cost + best_cost[end];
1806 if candidate < best_cost[start] {
1807 best_cost[start] = candidate;
1808 next_index[start] = Some(end);
1809 }
1810 }
1811 }
1812
1813 let mut wrapped = Vec::new();
1814 let mut current = 0usize;
1815 while current < node_count - 1 {
1816 let next = next_index[current]?;
1817 let start_byte = boundaries[breakpoints[current]];
1818 let end_byte = boundaries[breakpoints[next]];
1819 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1820 if trimmed_end <= start_byte {
1821 return None;
1822 }
1823 let segment_start_idx = breakpoints[current];
1824 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1825 wrapped.push(measure_context.display_line_for_char_range(
1826 &boundaries,
1827 segment_start_idx,
1828 segment_end_idx,
1829 ));
1830 current = next;
1831 }
1832
1833 if wrapped.is_empty() {
1834 return None;
1835 }
1836
1837 Some(wrapped)
1838}
1839
1840fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1841 let mut points = vec![0usize];
1842 for idx in 1..boundaries.len() - 1 {
1843 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1844 let current = &line[boundaries[idx]..boundaries[idx + 1]];
1845 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1846 points.push(idx);
1847 }
1848 }
1849 let end = boundaries.len() - 1;
1850 if points.last().copied() != Some(end) {
1851 points.push(end);
1852 }
1853 points
1854}
1855
1856fn choose_wrap_break(
1857 line: &str,
1858 boundaries: &[usize],
1859 start_idx: usize,
1860 best: usize,
1861 _line_break: LineBreak,
1862) -> usize {
1863 if best >= boundaries.len() - 1 {
1864 return best;
1865 }
1866
1867 if best <= start_idx + 1 {
1868 return best;
1869 }
1870
1871 for idx in (start_idx + 1..=best).rev() {
1872 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1873 if prev.chars().all(char::is_whitespace) {
1874 return idx;
1875 }
1876 }
1877 best
1878}
1879
1880fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1881 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1882 return false;
1883 }
1884 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1885 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1886 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1887}
1888
1889fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1890 measurer: &M,
1891 line: &str,
1892 style: &TextStyle,
1893 boundaries: &[usize],
1894 start_idx: usize,
1895 break_idx: usize,
1896) -> usize {
1897 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
1898 && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
1899 {
1900 return candidate;
1901 }
1902 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1903}
1904
1905fn is_valid_auto_hyphen_break(
1906 line: &str,
1907 boundaries: &[usize],
1908 start_idx: usize,
1909 break_idx: usize,
1910 candidate_idx: usize,
1911) -> bool {
1912 let end_idx = boundaries.len().saturating_sub(1);
1913 candidate_idx > start_idx
1914 && candidate_idx < end_idx
1915 && candidate_idx <= break_idx
1916 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1917 && is_break_inside_word(line, boundaries, candidate_idx)
1918}
1919
1920fn choose_auto_hyphen_break_fallback(
1921 boundaries: &[usize],
1922 start_idx: usize,
1923 break_idx: usize,
1924) -> usize {
1925 let end_idx = boundaries.len().saturating_sub(1);
1926 if break_idx >= end_idx {
1927 return break_idx;
1928 }
1929 let trailing_len = end_idx.saturating_sub(break_idx);
1930 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1931 return break_idx;
1932 }
1933
1934 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1935 let max_break = break_idx.saturating_sub(1);
1936 if min_break > max_break {
1937 return break_idx;
1938 }
1939
1940 let mut best_break = break_idx;
1941 let mut best_penalty = usize::MAX;
1942 for idx in min_break..=max_break {
1943 let candidate_trailing_len = end_idx.saturating_sub(idx);
1944 let candidate_prefix_len = idx.saturating_sub(start_idx);
1945 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1946 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1947 {
1948 continue;
1949 }
1950
1951 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1952 if penalty < best_penalty {
1953 best_penalty = penalty;
1954 best_break = idx;
1955 if penalty == 0 {
1956 break;
1957 }
1958 }
1959 }
1960 best_break
1961}
1962
1963fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
1964 while idx < boundaries.len() - 1 {
1965 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
1966 if !ch.chars().all(char::is_whitespace) {
1967 break;
1968 }
1969 idx += 1;
1970 }
1971 idx
1972}
1973
1974fn apply_overflow<M: TextMeasurer + ?Sized>(
1975 measurer: &M,
1976 node_id: Option<NodeId>,
1977 text: &crate::text::AnnotatedString,
1978 style: &TextStyle,
1979 options: TextLayoutOptions,
1980 max_width: Option<f32>,
1981 visible_lines: &mut Vec<DisplayLine>,
1982) -> bool {
1983 if options.overflow == TextOverflow::Visible {
1984 return false;
1985 }
1986 let ellipsis = EllipsisPlacement::for_options(options);
1987 let mut did_overflow = false;
1988 if visible_lines.len() > options.max_lines {
1989 did_overflow = true;
1990 visible_lines.truncate(options.max_lines);
1991 if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
1992 last_line.extend_to_paragraph_end(text);
1993 last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
1994 }
1995 }
1996
1997 let Some(width_limit) = max_width else {
1998 return did_overflow;
1999 };
2000 let visible_len = visible_lines.len();
2001 for (line_index, line) in visible_lines.iter_mut().enumerate() {
2002 if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2003 continue;
2004 }
2005 did_overflow = true;
2006 if line_index + 1 == visible_len
2007 && let Some(placement) = ellipsis
2008 {
2009 line.ellipsize(measurer, node_id, text, style, max_width, placement);
2010 }
2011 }
2012 did_overflow
2013}
2014
2015#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2016enum EllipsisPlacement {
2017 End,
2018 Start,
2019 Middle,
2020}
2021
2022impl EllipsisPlacement {
2023 fn for_options(options: TextLayoutOptions) -> Option<Self> {
2024 let single_line = options.max_lines == 1;
2025 match options.overflow {
2026 TextOverflow::Ellipsis => Some(Self::End),
2027 TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2028 TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2029 TextOverflow::StartEllipsis
2030 | TextOverflow::MiddleEllipsis
2031 | TextOverflow::Clip
2032 | TextOverflow::Visible
2033 | TextOverflow::ScaleDown { .. } => None,
2034 }
2035 }
2036
2037 fn elide(
2038 self,
2039 source: &crate::text::AnnotatedString,
2040 source_range: Range<usize>,
2041 boundaries: &[usize],
2042 kept_chars: usize,
2043 ) -> crate::text::AnnotatedString {
2044 let char_count = boundaries.len() - 1;
2045 let (head_chars, tail_chars) = match self {
2046 Self::End => (kept_chars, 0),
2047 Self::Start => (0, kept_chars),
2048 Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2049 };
2050 let head_end = source_range.start + boundaries[head_chars];
2051 let tail_start = source_range.start + boundaries[char_count - tail_chars];
2052 crate::text::AnnotatedString::builder()
2053 .append_annotated_subsequence(source, source_range.start..head_end)
2054 .append(ELLIPSIS)
2055 .append_annotated_subsequence(source, tail_start..source_range.end)
2056 .to_annotated_string()
2057 }
2058}
2059
2060fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2061 measurer: &M,
2062 node_id: Option<NodeId>,
2063 source: &crate::text::AnnotatedString,
2064 source_range: Range<usize>,
2065 style: &TextStyle,
2066 max_width: Option<f32>,
2067 placement: EllipsisPlacement,
2068) -> DisplayLine {
2069 let width_limit = max_width.unwrap_or(f32::INFINITY);
2070 let fitting_line = |text: DisplayLineText| {
2071 let mut line = DisplayLine {
2072 source_range: source_range.clone(),
2073 text,
2074 measured_width: None,
2075 };
2076 let width = line.measure_width(measurer, node_id, source, style);
2077 line.measured_width = Some(width);
2078 (width <= width_limit + WRAP_EPSILON).then_some(line)
2079 };
2080 if placement != EllipsisPlacement::End
2081 && let Some(line) = fitting_line(DisplayLineText::Source)
2082 {
2083 return line;
2084 }
2085
2086 let boundaries = char_boundaries(&source.text[source_range.clone()]);
2087 let elided_line = |kept_chars: usize| {
2088 fitting_line(DisplayLineText::Ellipsized(placement.elide(
2089 source,
2090 source_range.clone(),
2091 &boundaries,
2092 kept_chars,
2093 )))
2094 };
2095 let Some(mut best) = elided_line(0) else {
2096 return DisplayLine {
2097 source_range: source_range.clone(),
2098 text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2099 measured_width: None,
2100 };
2101 };
2102
2103 let mut fitting = 0usize;
2104 let mut overflowing = boundaries.len();
2105 while fitting + 1 < overflowing {
2106 let kept_chars = fitting + (overflowing - fitting) / 2;
2107 match elided_line(kept_chars) {
2108 Some(line) => {
2109 fitting = kept_chars;
2110 best = line;
2111 }
2112 None => overflowing = kept_chars,
2113 }
2114 }
2115 best
2116}
2117
2118fn char_boundaries(text: &str) -> Vec<usize> {
2119 let mut out = Vec::with_capacity(text.chars().count() + 1);
2120 out.push(0);
2121 for (idx, _) in text.char_indices() {
2122 if idx != 0 {
2123 out.push(idx);
2124 }
2125 }
2126 out.push(text.len());
2127 out
2128}
2129
2130#[cfg(test)]
2131#[path = "tests/measure_tests.rs"]
2132mod tests;