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: Rc<crate::text::AnnotatedString>,
48 pub visual_style: TextStyle,
49 pub metrics: TextMetrics,
50 pub did_overflow: bool,
51}
52
53#[derive(Clone, Debug, PartialEq)]
54pub struct TextLinePrefixWidths {
55 prefix_widths: Vec<f32>,
56 separator_before: Vec<f32>,
57 non_empty_overhang: f32,
58}
59
60impl TextLinePrefixWidths {
61 pub fn from_parts(
62 prefix_widths: Vec<f32>,
63 separator_before: Vec<f32>,
64 non_empty_overhang: f32,
65 ) -> Option<Self> {
66 if prefix_widths.is_empty() || prefix_widths.len() != separator_before.len() + 1 {
67 return None;
68 }
69 if prefix_widths
70 .iter()
71 .chain(separator_before.iter())
72 .any(|value| !value.is_finite())
73 {
74 return None;
75 }
76 let non_empty_overhang = non_empty_overhang.max(0.0);
77 if !non_empty_overhang.is_finite() {
78 return None;
79 }
80 Some(Self {
81 prefix_widths,
82 separator_before,
83 non_empty_overhang,
84 })
85 }
86
87 pub fn monospaced(char_count: usize, char_width: f32, letter_spacing: f32) -> Option<Self> {
88 if !char_width.is_finite() || !letter_spacing.is_finite() {
89 return None;
90 }
91 let char_width = char_width.max(0.0);
92 let letter_spacing = letter_spacing.max(0.0);
93 let mut prefix_widths = Vec::with_capacity(char_count + 1);
94 let mut separator_before = Vec::with_capacity(char_count);
95 let mut width = 0.0f32;
96 prefix_widths.push(width);
97 for _ in 0..char_count {
98 separator_before.push(0.0);
99 width += char_width + letter_spacing;
100 prefix_widths.push(width);
101 }
102 Self::from_parts(prefix_widths, separator_before, 0.0)
103 }
104
105 pub fn char_count(&self) -> usize {
106 self.separator_before.len()
107 }
108
109 pub fn width_for_char_range(&self, start: usize, end: usize) -> Option<f32> {
110 if start > end || end > self.char_count() {
111 return None;
112 }
113 if start == end {
114 return Some(0.0);
115 }
116 let separator = self.separator_before.get(start).copied().unwrap_or(0.0);
117 Some(
118 (self.prefix_widths[end] - self.prefix_widths[start] - separator).max(0.0)
119 + self.non_empty_overhang,
120 )
121 }
122}
123
124pub trait TextMeasurer: 'static {
125 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
126
127 fn measure_for_node(
128 &self,
129 node_id: Option<NodeId>,
130 text: &crate::text::AnnotatedString,
131 style: &TextStyle,
132 ) -> TextMetrics {
133 let _ = node_id;
134 self.measure(text, style)
135 }
136
137 fn measure_subsequence(
138 &self,
139 text: &crate::text::AnnotatedString,
140 range: Range<usize>,
141 style: &TextStyle,
142 ) -> TextMetrics {
143 self.measure(&text.subsequence(range), style)
144 }
145
146 fn measure_subsequence_for_node(
147 &self,
148 node_id: Option<NodeId>,
149 text: &crate::text::AnnotatedString,
150 range: Range<usize>,
151 style: &TextStyle,
152 ) -> TextMetrics {
153 let _ = node_id;
154 self.measure_subsequence(text, range, style)
155 }
156
157 fn measure_line_prefix_widths(
158 &self,
159 text: &crate::text::AnnotatedString,
160 line_range: Range<usize>,
161 style: &TextStyle,
162 ) -> Option<TextLinePrefixWidths> {
163 let _ = text;
164 let _ = line_range;
165 let _ = style;
166 None
167 }
168
169 fn measure_line_width(
170 &self,
171 text: &crate::text::AnnotatedString,
172 line_range: Range<usize>,
173 style: &TextStyle,
174 ) -> Option<f32> {
175 let _ = text;
176 let _ = line_range;
177 let _ = style;
178 None
179 }
180
181 fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
182 self.measure(text, style).line_height
183 }
184
185 fn glyph_line_box(&self, style: &TextStyle) -> Option<(f32, f32)> {
194 let _ = style;
195 None
196 }
197
198 fn first_baseline(&self, style: &TextStyle) -> Option<f32> {
204 let _ = style;
205 None
206 }
207
208 fn line_box(&self, style: &TextStyle) -> Option<crate::text::LineBox> {
216 let baseline = self.first_baseline(style)?;
217 Some(crate::text::LineBox {
218 height: self.line_height(&crate::text::AnnotatedString::default(), style),
219 baseline,
220 })
221 }
222
223 fn line_height_for_node(
224 &self,
225 node_id: Option<NodeId>,
226 text: &crate::text::AnnotatedString,
227 style: &TextStyle,
228 ) -> f32 {
229 let _ = node_id;
230 self.line_height(text, style)
231 }
232
233 fn get_offset_for_position(
234 &self,
235 text: &crate::text::AnnotatedString,
236 style: &TextStyle,
237 x: f32,
238 y: f32,
239 ) -> usize;
240
241 fn get_cursor_x_for_offset(
242 &self,
243 text: &crate::text::AnnotatedString,
244 style: &TextStyle,
245 offset: usize,
246 ) -> f32;
247
248 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
249
250 fn choose_auto_hyphen_break(
256 &self,
257 _line: &str,
258 _style: &TextStyle,
259 _segment_start_char: usize,
260 _measured_break_char: usize,
261 ) -> Option<usize> {
262 None
263 }
264
265 fn measure_with_options(
266 &self,
267 text: &crate::text::AnnotatedString,
268 style: &TextStyle,
269 options: TextLayoutOptions,
270 max_width: Option<f32>,
271 ) -> TextMetrics {
272 self.prepare_with_options(text, style, options, max_width)
273 .metrics
274 }
275
276 fn measure_with_options_for_node(
277 &self,
278 node_id: Option<NodeId>,
279 text: &crate::text::AnnotatedString,
280 style: &TextStyle,
281 options: TextLayoutOptions,
282 max_width: Option<f32>,
283 ) -> TextMetrics {
284 self.prepare_with_options_for_node(node_id, text, style, options, max_width)
285 .metrics
286 }
287
288 fn prepare_with_options(
289 &self,
290 text: &crate::text::AnnotatedString,
291 style: &TextStyle,
292 options: TextLayoutOptions,
293 max_width: Option<f32>,
294 ) -> PreparedTextLayout {
295 self.prepare_with_options_fallback(text, style, options, max_width)
296 }
297
298 fn prepare_with_options_for_node(
299 &self,
300 node_id: Option<NodeId>,
301 text: &crate::text::AnnotatedString,
302 style: &TextStyle,
303 options: TextLayoutOptions,
304 max_width: Option<f32>,
305 ) -> PreparedTextLayout {
306 prepare_text_layout_with_measurer_for_node(self, node_id, text, style, options, max_width)
307 }
308
309 fn prepare_with_options_fallback(
310 &self,
311 text: &crate::text::AnnotatedString,
312 style: &TextStyle,
313 options: TextLayoutOptions,
314 max_width: Option<f32>,
315 ) -> PreparedTextLayout {
316 prepare_text_layout_fallback(self, text, style, options, max_width)
317 }
318}
319
320#[derive(Default)]
321struct MonospacedTextMeasurer;
322
323impl MonospacedTextMeasurer {
324 const DEFAULT_SIZE: f32 = 14.0;
325 const CHAR_WIDTH_RATIO: f32 = 0.6;
326
327 fn get_metrics(style: &TextStyle) -> (f32, f32) {
328 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
329 let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
330 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
331 (
332 (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
333 line_height,
334 )
335 }
336}
337
338impl TextMeasurer for MonospacedTextMeasurer {
339 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
340 let (char_width, line_height) = Self::get_metrics(style);
341
342 let lines: Vec<&str> = text.text.split('\n').collect();
343 let line_count = lines.len().max(1);
344
345 let width = lines
346 .iter()
347 .map(|line| line.chars().count() as f32 * char_width)
348 .fold(0.0_f32, f32::max);
349
350 TextMetrics {
351 width,
352 height: line_count as f32 * line_height,
353 line_height,
354 line_count,
355 }
356 }
357
358 fn measure_subsequence(
359 &self,
360 text: &crate::text::AnnotatedString,
361 range: Range<usize>,
362 style: &TextStyle,
363 ) -> TextMetrics {
364 let (char_width, line_height) = Self::get_metrics(style);
365 let slice = &text.text[range];
366 let line_count = slice.split('\n').count().max(1);
367 let width = slice
368 .split('\n')
369 .map(|line| line.chars().count() as f32 * char_width)
370 .fold(0.0_f32, f32::max);
371
372 TextMetrics {
373 width,
374 height: line_count as f32 * line_height,
375 line_height,
376 line_count,
377 }
378 }
379
380 fn measure_line_prefix_widths(
381 &self,
382 text: &crate::text::AnnotatedString,
383 line_range: Range<usize>,
384 style: &TextStyle,
385 ) -> Option<TextLinePrefixWidths> {
386 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
387 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE);
388 TextLinePrefixWidths::monospaced(
389 text.text[line_range].chars().count(),
390 font_size * Self::CHAR_WIDTH_RATIO,
391 letter_spacing,
392 )
393 }
394
395 fn measure_line_width(
396 &self,
397 text: &crate::text::AnnotatedString,
398 line_range: Range<usize>,
399 style: &TextStyle,
400 ) -> Option<f32> {
401 Some(self.measure_subsequence(text, line_range, style).width)
402 }
403
404 fn line_height(&self, _text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
405 let (_, line_height) = Self::get_metrics(style);
406 line_height
407 }
408
409 fn get_offset_for_position(
410 &self,
411 text: &crate::text::AnnotatedString,
412 style: &TextStyle,
413 x: f32,
414 y: f32,
415 ) -> usize {
416 let (char_width, line_height) = Self::get_metrics(style);
417
418 if text.text.is_empty() {
419 return 0;
420 }
421
422 let line_index = (y / line_height).floor().max(0.0) as usize;
423 let lines: Vec<&str> = text.text.split('\n').collect();
424 let target_line = line_index.min(lines.len().saturating_sub(1));
425
426 let mut line_start_byte = 0;
427 for line in lines.iter().take(target_line) {
428 line_start_byte += line.len() + 1;
429 }
430
431 let line_text = lines.get(target_line).unwrap_or(&"");
432 let char_index = (x / char_width).round() as usize;
433 let line_char_count = line_text.chars().count();
434 let clamped_index = char_index.min(line_char_count);
435
436 let offset_in_line = line_text
437 .char_indices()
438 .nth(clamped_index)
439 .map_or(line_text.len(), |(i, _)| i);
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_or((0.0, line_height), |(off, h)| {
720 (off.min(line_height), h.min(line_height))
721 })
722}
723
724pub fn first_baseline(style: &TextStyle) -> Option<f32> {
728 let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
729 crate::render_state::with_text_service(|service| {
730 service.with_measurer(|m| m.first_baseline(&style))
731 })
732}
733
734pub fn measure_text_for_node(
735 node_id: Option<NodeId>,
736 text: &crate::text::AnnotatedString,
737 style: &TextStyle,
738) -> TextMetrics {
739 with_system_font_scale(text, style, |text, style| {
740 crate::render_state::with_text_service(|service| service.measure(node_id, text, style))
741 })
742}
743
744pub fn measure_text_with_options(
745 text: &crate::text::AnnotatedString,
746 style: &TextStyle,
747 options: TextLayoutOptions,
748 max_width: Option<f32>,
749) -> TextMetrics {
750 with_system_font_scale(text, style, |text, style| {
751 crate::render_state::with_text_service(|service| {
752 service.measure_with_options(None, text, style, options.normalized(), max_width)
753 })
754 })
755}
756
757pub fn measure_text_with_options_for_node(
758 node_id: Option<NodeId>,
759 text: &crate::text::AnnotatedString,
760 style: &TextStyle,
761 options: TextLayoutOptions,
762 max_width: Option<f32>,
763) -> TextMetrics {
764 with_system_font_scale(text, style, |text, style| {
765 crate::render_state::with_text_service(|service| {
766 service.measure_with_options(node_id, text, style, options.normalized(), max_width)
767 })
768 })
769}
770
771pub fn prepare_text_layout(
772 text: &crate::text::AnnotatedString,
773 style: &TextStyle,
774 options: TextLayoutOptions,
775 max_width: Option<f32>,
776) -> PreparedTextLayout {
777 with_system_font_scale(text, style, |text, style| {
778 crate::render_state::with_text_service(|service| {
779 service.prepare_with_options(None, text, style, options.normalized(), max_width)
780 })
781 })
782}
783
784pub fn prepare_text_layout_for_node(
785 node_id: Option<NodeId>,
786 text: &crate::text::AnnotatedString,
787 style: &TextStyle,
788 options: TextLayoutOptions,
789 max_width: Option<f32>,
790) -> PreparedTextLayout {
791 with_system_font_scale(text, style, |text, style| {
792 crate::render_state::with_text_service(|service| {
793 service.prepare_with_options(node_id, text, style, options.normalized(), max_width)
794 })
795 })
796}
797
798pub fn get_offset_for_position(
799 text: &crate::text::AnnotatedString,
800 style: &TextStyle,
801 x: f32,
802 y: f32,
803) -> usize {
804 with_system_font_scale(text, style, |text, style| {
805 crate::render_state::with_text_measurer(|m| m.get_offset_for_position(text, style, x, y))
806 })
807}
808
809pub fn offset_for_position_wrapped(
823 text: &str,
824 style: &TextStyle,
825 node_id: Option<NodeId>,
826 wrap_width: Option<f32>,
827 line_height: f32,
828 x: f32,
829 y: f32,
830) -> usize {
831 if text.is_empty() {
832 return 0;
833 }
834 let annotated = crate::text::AnnotatedString::from(text);
835 let line_ranges = wrapped_line_ranges(
836 node_id,
837 &annotated,
838 style,
839 TextLayoutOptions::default(),
840 wrap_width,
841 );
842 if line_ranges.is_empty() {
843 return 0;
844 }
845 let line_idx = if line_height > 0.0 {
846 (y / line_height).floor().max(0.0) as usize
847 } else {
848 0
849 }
850 .min(line_ranges.len() - 1);
851 let range = &line_ranges[line_idx];
852 let line = &text[range.start..range.end];
853 let within = get_offset_for_position(&crate::text::AnnotatedString::from(line), style, x, 0.0);
854 range.start + within.min(line.len())
855}
856
857pub fn get_cursor_x_for_offset(
858 text: &crate::text::AnnotatedString,
859 style: &TextStyle,
860 offset: usize,
861) -> f32 {
862 with_system_font_scale(text, style, |text, style| {
863 crate::render_state::with_text_measurer(|m| m.get_cursor_x_for_offset(text, style, offset))
864 })
865}
866
867pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
868 with_system_font_scale(text, style, |text, style| {
869 crate::render_state::with_text_service(|service| service.layout(text, style))
870 })
871}
872
873pub fn wrapped_line_ranges(
886 node_id: Option<NodeId>,
887 text: &crate::text::AnnotatedString,
888 style: &TextStyle,
889 options: TextLayoutOptions,
890 max_width: Option<f32>,
891) -> Vec<Range<usize>> {
892 with_system_font_scale(text, style, |text, style| {
893 crate::render_state::with_text_measurer(|m| {
894 wrapped_line_ranges_with_measurer(m, node_id, text, style, options, max_width)
895 })
896 })
897}
898
899fn wrapped_line_ranges_with_measurer<M: TextMeasurer + ?Sized>(
900 measurer: &M,
901 _node_id: Option<NodeId>,
902 text: &crate::text::AnnotatedString,
903 style: &TextStyle,
904 options: TextLayoutOptions,
905 max_width: Option<f32>,
906) -> Vec<Range<usize>> {
907 let opts = options.normalized();
908 let max_width = normalize_max_width(max_width);
909 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
910 .then_some(max_width)
911 .flatten();
912 let line_break_mode = style
913 .paragraph_style
914 .line_break
915 .take_or_else(|| LineBreak::Simple);
916 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
917
918 let line_ranges = split_line_ranges(text.text.as_str());
919 let Some(width_limit) = wrap_width else {
920 return line_ranges;
921 };
922 let mut ranges = Vec::with_capacity(line_ranges.len());
923 for line_range in line_ranges {
924 for display_line in wrap_line_to_width(
925 measurer,
926 text,
927 line_range,
928 style,
929 width_limit,
930 line_break_mode,
931 hyphens_mode,
932 ) {
933 ranges.push(display_line.source_range.clone());
934 }
935 }
936 ranges
937}
938
939fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
940 measurer: &M,
941 text: &crate::text::AnnotatedString,
942 style: &TextStyle,
943 options: TextLayoutOptions,
944 max_width: Option<f32>,
945) -> PreparedTextLayout {
946 prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
947}
948
949pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
950 measurer: &M,
951 node_id: Option<NodeId>,
952 text: &crate::text::AnnotatedString,
953 style: &TextStyle,
954 options: TextLayoutOptions,
955 max_width: Option<f32>,
956) -> PreparedTextLayout {
957 let telemetry = text_layout_telemetry_enabled();
958 let total_start = telemetry.then(Instant::now);
959 let opts = options.normalized();
960 let max_width = normalize_max_width(max_width);
961 if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
962 return prepare_scale_down_text_layout(
963 measurer,
964 node_id,
965 text,
966 style,
967 opts,
968 max_width,
969 min_font_size_sp,
970 );
971 }
972
973 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
974 .then_some(max_width)
975 .flatten();
976 let line_break_mode = style
977 .paragraph_style
978 .line_break
979 .take_or_else(|| LineBreak::Simple);
980 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
981
982 let wrap_start = telemetry.then(Instant::now);
983 let line_ranges = split_line_ranges(text.text.as_str());
984 let source_line_count = line_ranges.len();
985 let mut visible_lines: Vec<DisplayLine>;
986 if let Some(width_limit) = wrap_width {
987 visible_lines = Vec::with_capacity(line_ranges.len());
988 for line_range in line_ranges {
989 let wrapped_lines = wrap_line_to_width(
990 measurer,
991 text,
992 line_range,
993 style,
994 width_limit,
995 line_break_mode,
996 hyphens_mode,
997 );
998 visible_lines.extend(wrapped_lines);
999 }
1000 } else {
1001 visible_lines = line_ranges
1002 .into_iter()
1003 .map(DisplayLine::from_source_range)
1004 .collect();
1005 }
1006 let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1007
1008 let overflow_start = telemetry.then(Instant::now);
1009 let did_overflow = apply_overflow(
1010 measurer,
1011 node_id,
1012 text,
1013 style,
1014 opts,
1015 max_width,
1016 &mut visible_lines,
1017 );
1018 let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1019
1020 let build_start = telemetry.then(Instant::now);
1021 let display_annotated = build_display_annotated(text, &visible_lines);
1022 debug_assert_eq!(
1023 display_annotated.text,
1024 join_display_line_text(text, &visible_lines)
1025 );
1026 let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1027
1028 let metrics_start = telemetry.then(Instant::now);
1029 let line_height = measurer.line_height_for_node(node_id, text, style).max(0.0);
1030 let display_line_count = visible_lines.len().max(1);
1031 let layout_line_count = display_line_count.max(opts.min_lines);
1032
1033 let measured_width = if visible_lines.is_empty() {
1034 0.0
1035 } else {
1036 visible_lines
1037 .iter()
1038 .map(|line| line.measure_width(measurer, node_id, text, style))
1039 .fold(0.0_f32, f32::max)
1040 };
1041 let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1042 let width = if opts.overflow == TextOverflow::Visible {
1043 measured_width
1044 } else if let Some(width_limit) = max_width {
1045 measured_width.min(width_limit)
1046 } else {
1047 measured_width
1048 };
1049
1050 let prepared = PreparedTextLayout {
1051 text: Rc::new(display_annotated),
1052 visual_style: style.clone(),
1053 metrics: TextMetrics {
1054 width,
1055 height: layout_line_count as f32 * line_height,
1056 line_height,
1057 line_count: layout_line_count,
1058 },
1059 did_overflow,
1060 };
1061
1062 if let Some(start) = total_start {
1063 eprintln!(
1064 "[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}",
1065 text.text.len(),
1066 text.span_styles.len(),
1067 source_line_count,
1068 display_line_count,
1069 wrap_width.is_some(),
1070 max_width,
1071 wrap_ms.unwrap_or(0.0),
1072 overflow_ms.unwrap_or(0.0),
1073 build_ms.unwrap_or(0.0),
1074 metrics_ms.unwrap_or(0.0),
1075 start.elapsed().as_secs_f64() * 1000.0,
1076 );
1077 }
1078
1079 prepared
1080}
1081
1082fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1083 measurer: &M,
1084 node_id: Option<NodeId>,
1085 text: &crate::text::AnnotatedString,
1086 style: &TextStyle,
1087 options: TextLayoutOptions,
1088 max_width: Option<f32>,
1089 min_font_size_sp: f32,
1090) -> PreparedTextLayout {
1091 let clipped_options = TextLayoutOptions {
1092 overflow: TextOverflow::Clip,
1093 ..options
1094 }
1095 .normalized();
1096
1097 let full_size = prepare_scaled_text_layout(
1098 measurer,
1099 node_id,
1100 text,
1101 style,
1102 clipped_options,
1103 max_width,
1104 FontScaleCurve::linear(1.0),
1105 );
1106 let Some(width_limit) = max_width else {
1107 return full_size;
1108 };
1109 if !full_size.did_overflow {
1110 return full_size;
1111 }
1112
1113 let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1114 if !base_font_size.is_finite() || base_font_size <= 0.0 {
1115 return full_size;
1116 }
1117 let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1118 if min_scale >= 1.0 {
1119 return full_size;
1120 }
1121
1122 let min_size = prepare_scaled_text_layout(
1123 measurer,
1124 node_id,
1125 text,
1126 style,
1127 clipped_options,
1128 Some(width_limit),
1129 FontScaleCurve::linear(min_scale),
1130 );
1131 if min_size.did_overflow {
1132 return min_size;
1133 }
1134
1135 let mut low = min_scale;
1136 let mut high = 1.0;
1137 let mut best = min_size;
1138 for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1139 let mid = (low + high) * 0.5;
1140 let candidate = prepare_scaled_text_layout(
1141 measurer,
1142 node_id,
1143 text,
1144 style,
1145 clipped_options,
1146 Some(width_limit),
1147 FontScaleCurve::linear(mid),
1148 );
1149 if candidate.did_overflow {
1150 high = mid;
1151 } else {
1152 low = mid;
1153 best = candidate;
1154 }
1155 }
1156
1157 best
1158}
1159
1160fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1161 measurer: &M,
1162 node_id: Option<NodeId>,
1163 text: &crate::text::AnnotatedString,
1164 style: &TextStyle,
1165 options: TextLayoutOptions,
1166 max_width: Option<f32>,
1167 shrink: FontScaleCurve,
1168) -> PreparedTextLayout {
1169 let visual_style = scale_text_style_font_sizes(style, shrink);
1170 let visual_text = scale_annotated_font_sizes(text, shrink);
1171 prepare_text_layout_with_measurer_for_node(
1172 measurer,
1173 node_id,
1174 visual_text.as_ref(),
1175 &visual_style,
1176 options,
1177 max_width,
1178 )
1179}
1180
1181fn scale_annotated_font_sizes(
1182 text: &crate::text::AnnotatedString,
1183 curve: FontScaleCurve,
1184) -> Cow<'_, crate::text::AnnotatedString> {
1185 if curve.is_identity() || !annotated_text_needs_scaling(text) {
1186 return Cow::Borrowed(text);
1187 }
1188
1189 let mut scaled = text.clone();
1190 for span in &mut scaled.span_styles {
1191 span.item = scale_span_style_font_sizes(&span.item, curve, None);
1192 }
1193 Cow::Owned(scaled)
1194}
1195
1196fn scale_text_style_font_sizes(style: &TextStyle, curve: FontScaleCurve) -> TextStyle {
1197 if curve.is_identity() {
1198 return style.clone();
1199 }
1200
1201 let mut scaled = style.clone();
1202 scaled.span_style =
1203 scale_span_style_font_sizes(&style.span_style, curve, Some(DEFAULT_FONT_SIZE_SP));
1204 scaled.paragraph_style.line_height =
1205 scale_text_unit_sp(scaled.paragraph_style.line_height, curve);
1206 if let Some(mut indent) = scaled.paragraph_style.text_indent {
1207 indent.first_line = scale_text_unit_sp(indent.first_line, curve);
1208 indent.rest_line = scale_text_unit_sp(indent.rest_line, curve);
1209 scaled.paragraph_style.text_indent = Some(indent);
1210 }
1211 scaled
1212}
1213
1214fn with_system_font_scale<R>(
1215 text: &crate::text::AnnotatedString,
1216 style: &TextStyle,
1217 block: impl FnOnce(&crate::text::AnnotatedString, &TextStyle) -> R,
1218) -> R {
1219 let curve = crate::current_font_scale_curve();
1220 let visual_style = scale_text_style_font_sizes(style, curve);
1221 let visual_text = scale_annotated_font_sizes(text, curve);
1222 block(visual_text.as_ref(), &visual_style)
1223}
1224
1225fn scale_span_style_font_sizes(
1226 style: &crate::text::SpanStyle,
1227 curve: FontScaleCurve,
1228 default_font_size_sp: Option<f32>,
1229) -> crate::text::SpanStyle {
1230 let factor = curve.scale();
1231 let mut scaled = style.clone();
1232 scaled.font_size = match (style.font_size, default_font_size_sp) {
1233 (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1234 crate::text::TextUnit::Sp(curve.sp_to_dp(default_size))
1235 }
1236 (unit, Some(_)) => scale_text_unit_sp_and_em(unit, curve),
1237 (unit, None) => scale_text_unit_sp(unit, curve),
1238 };
1239 scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, curve);
1240 if let Some(mut shadow) = scaled.shadow {
1241 shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1242 shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1243 shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1244 scaled.shadow = Some(shadow);
1245 }
1246 if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1247 scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1248 width: width * factor,
1249 });
1250 }
1251 scaled
1252}
1253
1254fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1255 text.span_styles
1256 .iter()
1257 .any(|span| span_style_needs_scaling(&span.item))
1258}
1259
1260fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1261 matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1262 || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1263 || matches!(
1264 style.draw_style,
1265 Some(crate::text::TextDrawStyle::Stroke { .. })
1266 )
1267 || style.shadow.is_some()
1268}
1269
1270fn scale_text_unit_sp(unit: crate::text::TextUnit, curve: FontScaleCurve) -> crate::text::TextUnit {
1271 match unit {
1272 crate::text::TextUnit::Sp(value) if value.is_finite() => {
1273 crate::text::TextUnit::Sp(curve.sp_to_dp(value))
1274 }
1275 other => other,
1276 }
1277}
1278
1279fn scale_text_unit_sp_and_em(
1280 unit: crate::text::TextUnit,
1281 curve: FontScaleCurve,
1282) -> crate::text::TextUnit {
1283 match unit {
1284 crate::text::TextUnit::Sp(_) => scale_text_unit_sp(unit, curve),
1285 crate::text::TextUnit::Em(value) if value.is_finite() => {
1286 crate::text::TextUnit::Em(value * curve.scale())
1287 }
1288 other => other,
1289 }
1290}
1291
1292fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1293 if value.is_finite() {
1294 value * factor
1295 } else {
1296 value
1297 }
1298}
1299
1300#[derive(Clone, Debug)]
1301enum DisplayLineText {
1302 Source,
1303 Ellipsized(crate::text::AnnotatedString),
1304}
1305
1306#[derive(Clone, Debug)]
1307struct DisplayLine {
1308 source_range: Range<usize>,
1309 text: DisplayLineText,
1310 measured_width: Option<f32>,
1311}
1312
1313impl DisplayLine {
1314 fn from_source_range(source_range: Range<usize>) -> Self {
1315 Self {
1316 source_range,
1317 text: DisplayLineText::Source,
1318 measured_width: None,
1319 }
1320 }
1321
1322 fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1323 Self {
1324 source_range,
1325 text: DisplayLineText::Source,
1326 measured_width: measured_width
1327 .is_finite()
1328 .then_some(measured_width.max(0.0)),
1329 }
1330 }
1331
1332 fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1333 match &self.text {
1334 DisplayLineText::Source => &source.text[self.source_range.clone()],
1335 DisplayLineText::Ellipsized(annotated) => annotated.text.as_str(),
1336 }
1337 }
1338
1339 fn measure_width<M: TextMeasurer + ?Sized>(
1340 &self,
1341 measurer: &M,
1342 node_id: Option<NodeId>,
1343 source: &crate::text::AnnotatedString,
1344 style: &TextStyle,
1345 ) -> f32 {
1346 self.measured_width.unwrap_or_else(|| match &self.text {
1347 DisplayLineText::Source => {
1348 measurer
1349 .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1350 .width
1351 }
1352 DisplayLineText::Ellipsized(annotated) => {
1353 measurer.measure_for_node(node_id, annotated, style).width
1354 }
1355 })
1356 }
1357
1358 fn extend_to_paragraph_end(&mut self, source: &crate::text::AnnotatedString) {
1359 let start = self.source_range.start;
1360 let end = source.text[start..]
1361 .find('\n')
1362 .map_or(source.text.len(), |offset| start + offset);
1363 self.source_range = start..end;
1364 self.text = DisplayLineText::Source;
1365 self.measured_width = None;
1366 }
1367
1368 fn ellipsize<M: TextMeasurer + ?Sized>(
1369 &mut self,
1370 measurer: &M,
1371 node_id: Option<NodeId>,
1372 source: &crate::text::AnnotatedString,
1373 style: &TextStyle,
1374 max_width: Option<f32>,
1375 placement: EllipsisPlacement,
1376 ) {
1377 *self = fit_ellipsis(
1378 measurer,
1379 node_id,
1380 source,
1381 self.source_range.clone(),
1382 style,
1383 max_width,
1384 placement,
1385 );
1386 }
1387}
1388
1389fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1390 if text.is_empty() {
1391 return single_line_range(0..0);
1392 }
1393
1394 let mut ranges = Vec::new();
1395 let mut start = 0usize;
1396 for (idx, ch) in text.char_indices() {
1397 if ch == '\n' {
1398 ranges.push(start..idx);
1399 start = idx + ch.len_utf8();
1400 }
1401 }
1402 ranges.push(start..text.len());
1403 ranges
1404}
1405
1406fn build_display_annotated(
1407 source: &crate::text::AnnotatedString,
1408 lines: &[DisplayLine],
1409) -> crate::text::AnnotatedString {
1410 if lines.is_empty() {
1411 return crate::text::AnnotatedString::from("");
1412 }
1413
1414 let mut builder = crate::text::AnnotatedString::builder();
1415 for (idx, line) in lines.iter().enumerate() {
1416 builder = match &line.text {
1417 DisplayLineText::Source => {
1418 builder.append_annotated_subsequence(source, line.source_range.clone())
1419 }
1420 DisplayLineText::Ellipsized(annotated) => builder.append_annotated(annotated),
1421 };
1422 if idx + 1 < lines.len() {
1423 builder = builder.append("\n");
1424 }
1425 }
1426 builder.to_annotated_string()
1427}
1428
1429fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1430 let mut text = String::new();
1431 for (idx, line) in lines.iter().enumerate() {
1432 text.push_str(line.display_text(source));
1433 if idx + 1 < lines.len() {
1434 text.push('\n');
1435 }
1436 }
1437 text
1438}
1439
1440fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1441 while end > start {
1442 let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1443 break;
1444 };
1445 if ch.is_whitespace() {
1446 end = start + idx;
1447 } else {
1448 break;
1449 }
1450 }
1451 end
1452}
1453
1454fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1455 match max_width {
1456 Some(width) if width.is_finite() && width > 0.0 => Some(width),
1457 _ => None,
1458 }
1459}
1460
1461fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1462 (base_start + relative.start)..(base_start + relative.end)
1463}
1464
1465fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1466 boundaries
1467 .binary_search(&byte_offset)
1468 .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1469}
1470
1471fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1472 std::iter::once(range).collect()
1473}
1474
1475struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1476 measurer: &'a M,
1477 text: &'a crate::text::AnnotatedString,
1478 style: &'a TextStyle,
1479 line_start: usize,
1480 prefix_widths: Option<TextLinePrefixWidths>,
1481}
1482
1483impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1484 fn new(
1485 measurer: &'a M,
1486 text: &'a crate::text::AnnotatedString,
1487 line_range: &Range<usize>,
1488 style: &'a TextStyle,
1489 boundary_count: usize,
1490 ) -> Self {
1491 let expected_chars = boundary_count.saturating_sub(1);
1492 let prefix_widths = measurer
1493 .measure_line_prefix_widths(text, line_range.clone(), style)
1494 .filter(|widths| widths.char_count() == expected_chars);
1495 Self {
1496 measurer,
1497 text,
1498 style,
1499 line_start: line_range.start,
1500 prefix_widths,
1501 }
1502 }
1503
1504 fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1505 if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1506 return width;
1507 }
1508 let segment_range =
1509 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1510 self.measurer
1511 .measure_subsequence(self.text, segment_range, self.style)
1512 .width
1513 }
1514
1515 fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1516 if let Some(prefix_widths) = &self.prefix_widths
1517 && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1518 {
1519 return Some(width);
1520 }
1521 None
1522 }
1523
1524 fn display_line_for_char_range(
1525 &self,
1526 boundaries: &[usize],
1527 start_idx: usize,
1528 end_idx: usize,
1529 ) -> DisplayLine {
1530 let source_range =
1531 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1532 let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1533 DisplayLine::from_measured_source_range(source_range, measured_width)
1534 }
1535}
1536
1537fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1538 measurer: &M,
1539 text: &crate::text::AnnotatedString,
1540 line_range: Range<usize>,
1541 style: &TextStyle,
1542 max_width: f32,
1543 line_break: LineBreak,
1544 hyphens: Hyphens,
1545) -> Vec<DisplayLine> {
1546 let line_text = &text.text[line_range.clone()];
1547 if line_text.is_empty() {
1548 return vec![DisplayLine::from_source_range(
1549 line_range.start..line_range.start,
1550 )];
1551 }
1552
1553 if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
1554 && measured_width <= max_width + WRAP_EPSILON
1555 {
1556 return vec![DisplayLine::from_measured_source_range(
1557 line_range,
1558 measured_width,
1559 )];
1560 }
1561
1562 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1563 && line_text.chars().any(char::is_whitespace)
1564 && let Some(balanced) = wrap_line_with_word_balance(
1565 measurer,
1566 text,
1567 line_range.clone(),
1568 style,
1569 max_width,
1570 line_break,
1571 )
1572 {
1573 return balanced;
1574 }
1575
1576 wrap_line_greedy(
1577 measurer, text, line_range, style, max_width, line_break, hyphens,
1578 )
1579}
1580
1581fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1582 measurer: &M,
1583 text: &crate::text::AnnotatedString,
1584 line_range: Range<usize>,
1585 style: &TextStyle,
1586 max_width: f32,
1587 line_break: LineBreak,
1588 hyphens: Hyphens,
1589) -> Vec<DisplayLine> {
1590 let line_text = &text.text[line_range.clone()];
1591 let boundaries = char_boundaries(line_text);
1592 let measure_context =
1593 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1594 if let Some(measured_width) =
1595 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1596 && measured_width <= max_width + WRAP_EPSILON
1597 {
1598 return vec![DisplayLine::from_measured_source_range(
1599 line_range,
1600 measured_width,
1601 )];
1602 }
1603 let mut wrapped = Vec::new();
1604 let mut start_idx = 0usize;
1605
1606 while start_idx < boundaries.len() - 1 {
1607 let mut low = start_idx + 1;
1608 let mut high = boundaries.len() - 1;
1609 let mut best = start_idx + 1;
1610
1611 while low <= high {
1612 let mid = (low + high) / 2;
1613 let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1614 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1615 best = mid;
1616 low = mid + 1;
1617 } else {
1618 if mid == 0 {
1619 break;
1620 }
1621 high = mid - 1;
1622 }
1623 }
1624
1625 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1626 let mut effective_wrap_idx = wrap_idx;
1627 let can_hyphenate = hyphens == Hyphens::Auto
1628 && wrap_idx == best
1629 && best < boundaries.len() - 1
1630 && is_break_inside_word(line_text, &boundaries, wrap_idx);
1631 if can_hyphenate {
1632 effective_wrap_idx = resolve_auto_hyphen_break(
1633 measurer,
1634 line_text,
1635 style,
1636 &boundaries,
1637 start_idx,
1638 wrap_idx,
1639 );
1640 }
1641
1642 let broke_at_word_boundary = effective_wrap_idx > start_idx
1643 && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
1644 .chars()
1645 .all(char::is_whitespace);
1646 let segment_start = boundaries[start_idx];
1647 let mut segment_end = boundaries[effective_wrap_idx];
1648 if wrap_idx != best || broke_at_word_boundary {
1649 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1650 }
1651 let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1652 wrapped.push(measure_context.display_line_for_char_range(
1653 &boundaries,
1654 start_idx,
1655 segment_end_idx,
1656 ));
1657
1658 start_idx = if wrap_idx != best || broke_at_word_boundary {
1659 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1660 } else {
1661 effective_wrap_idx
1662 };
1663 }
1664
1665 if wrapped.is_empty() {
1666 wrapped.push(DisplayLine::from_source_range(
1667 line_range.start..line_range.start,
1668 ));
1669 }
1670
1671 wrapped
1672}
1673
1674fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1675 measurer: &M,
1676 text: &crate::text::AnnotatedString,
1677 line_range: Range<usize>,
1678 style: &TextStyle,
1679 max_width: f32,
1680 line_break: LineBreak,
1681) -> Option<Vec<DisplayLine>> {
1682 let line_text = &text.text[line_range.clone()];
1683 let boundaries = char_boundaries(line_text);
1684 let measure_context =
1685 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1686 if let Some(measured_width) =
1687 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1688 && measured_width <= max_width + WRAP_EPSILON
1689 {
1690 return Some(vec![DisplayLine::from_measured_source_range(
1691 line_range,
1692 measured_width,
1693 )]);
1694 }
1695 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1696 if breakpoints.len() <= 2 {
1697 return None;
1698 }
1699
1700 let node_count = breakpoints.len();
1701 let mut best_cost = vec![f32::INFINITY; node_count];
1702 let mut next_index = vec![None; node_count];
1703 best_cost[node_count - 1] = 0.0;
1704
1705 for start in (0..node_count - 1).rev() {
1706 for end in start + 1..node_count {
1707 let start_byte = boundaries[breakpoints[start]];
1708 let end_byte = boundaries[breakpoints[end]];
1709 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1710 if trimmed_end <= start_byte {
1711 continue;
1712 }
1713 let segment_start_idx = breakpoints[start];
1714 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1715 let segment_width =
1716 measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1717 if segment_width > max_width + WRAP_EPSILON {
1718 continue;
1719 }
1720 if !best_cost[end].is_finite() {
1721 continue;
1722 }
1723 let slack = (max_width - segment_width).max(0.0);
1724 let is_last = end == node_count - 1;
1725 let segment_cost = match line_break {
1726 LineBreak::Heading => slack * slack,
1727 LineBreak::Paragraph => {
1728 if is_last {
1729 slack * slack * 0.16
1730 } else {
1731 slack * slack
1732 }
1733 }
1734 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1735 };
1736 let candidate = segment_cost + best_cost[end];
1737 if candidate < best_cost[start] {
1738 best_cost[start] = candidate;
1739 next_index[start] = Some(end);
1740 }
1741 }
1742 }
1743
1744 let mut wrapped = Vec::new();
1745 let mut current = 0usize;
1746 while current < node_count - 1 {
1747 let next = next_index[current]?;
1748 let start_byte = boundaries[breakpoints[current]];
1749 let end_byte = boundaries[breakpoints[next]];
1750 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1751 if trimmed_end <= start_byte {
1752 return None;
1753 }
1754 let segment_start_idx = breakpoints[current];
1755 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1756 wrapped.push(measure_context.display_line_for_char_range(
1757 &boundaries,
1758 segment_start_idx,
1759 segment_end_idx,
1760 ));
1761 current = next;
1762 }
1763
1764 if wrapped.is_empty() {
1765 return None;
1766 }
1767
1768 Some(wrapped)
1769}
1770
1771fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1772 let mut points = vec![0usize];
1773 for idx in 1..boundaries.len() - 1 {
1774 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1775 let current = &line[boundaries[idx]..boundaries[idx + 1]];
1776 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1777 points.push(idx);
1778 }
1779 }
1780 let end = boundaries.len() - 1;
1781 if points.last().copied() != Some(end) {
1782 points.push(end);
1783 }
1784 points
1785}
1786
1787fn choose_wrap_break(
1788 line: &str,
1789 boundaries: &[usize],
1790 start_idx: usize,
1791 best: usize,
1792 _line_break: LineBreak,
1793) -> usize {
1794 if best >= boundaries.len() - 1 {
1795 return best;
1796 }
1797
1798 if best <= start_idx + 1 {
1799 return best;
1800 }
1801
1802 for idx in (start_idx + 1..=best).rev() {
1803 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1804 if prev.chars().all(char::is_whitespace) {
1805 return idx;
1806 }
1807 }
1808 best
1809}
1810
1811fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1812 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1813 return false;
1814 }
1815 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1816 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1817 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1818}
1819
1820fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1821 measurer: &M,
1822 line: &str,
1823 style: &TextStyle,
1824 boundaries: &[usize],
1825 start_idx: usize,
1826 break_idx: usize,
1827) -> usize {
1828 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
1829 && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
1830 {
1831 return candidate;
1832 }
1833 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1834}
1835
1836fn is_valid_auto_hyphen_break(
1837 line: &str,
1838 boundaries: &[usize],
1839 start_idx: usize,
1840 break_idx: usize,
1841 candidate_idx: usize,
1842) -> bool {
1843 let end_idx = boundaries.len().saturating_sub(1);
1844 candidate_idx > start_idx
1845 && candidate_idx < end_idx
1846 && candidate_idx <= break_idx
1847 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1848 && is_break_inside_word(line, boundaries, candidate_idx)
1849}
1850
1851fn choose_auto_hyphen_break_fallback(
1852 boundaries: &[usize],
1853 start_idx: usize,
1854 break_idx: usize,
1855) -> usize {
1856 let end_idx = boundaries.len().saturating_sub(1);
1857 if break_idx >= end_idx {
1858 return break_idx;
1859 }
1860 let trailing_len = end_idx.saturating_sub(break_idx);
1861 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1862 return break_idx;
1863 }
1864
1865 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1866 let max_break = break_idx.saturating_sub(1);
1867 if min_break > max_break {
1868 return break_idx;
1869 }
1870
1871 let mut best_break = break_idx;
1872 let mut best_penalty = usize::MAX;
1873 for idx in min_break..=max_break {
1874 let candidate_trailing_len = end_idx.saturating_sub(idx);
1875 let candidate_prefix_len = idx.saturating_sub(start_idx);
1876 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1877 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1878 {
1879 continue;
1880 }
1881
1882 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1883 if penalty < best_penalty {
1884 best_penalty = penalty;
1885 best_break = idx;
1886 if penalty == 0 {
1887 break;
1888 }
1889 }
1890 }
1891 best_break
1892}
1893
1894fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
1895 while idx < boundaries.len() - 1 {
1896 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
1897 if !ch.chars().all(char::is_whitespace) {
1898 break;
1899 }
1900 idx += 1;
1901 }
1902 idx
1903}
1904
1905fn apply_overflow<M: TextMeasurer + ?Sized>(
1906 measurer: &M,
1907 node_id: Option<NodeId>,
1908 text: &crate::text::AnnotatedString,
1909 style: &TextStyle,
1910 options: TextLayoutOptions,
1911 max_width: Option<f32>,
1912 visible_lines: &mut Vec<DisplayLine>,
1913) -> bool {
1914 if options.overflow == TextOverflow::Visible {
1915 return false;
1916 }
1917 let ellipsis = EllipsisPlacement::for_options(options);
1918 let mut did_overflow = false;
1919 if visible_lines.len() > options.max_lines {
1920 did_overflow = true;
1921 visible_lines.truncate(options.max_lines);
1922 if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
1923 last_line.extend_to_paragraph_end(text);
1924 last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
1925 }
1926 }
1927
1928 let Some(width_limit) = max_width else {
1929 return did_overflow;
1930 };
1931 let visible_len = visible_lines.len();
1932 for (line_index, line) in visible_lines.iter_mut().enumerate() {
1933 if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
1934 continue;
1935 }
1936 did_overflow = true;
1937 if line_index + 1 == visible_len
1938 && let Some(placement) = ellipsis
1939 {
1940 line.ellipsize(measurer, node_id, text, style, max_width, placement);
1941 }
1942 }
1943 did_overflow
1944}
1945
1946#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1947enum EllipsisPlacement {
1948 End,
1949 Start,
1950 Middle,
1951}
1952
1953impl EllipsisPlacement {
1954 fn for_options(options: TextLayoutOptions) -> Option<Self> {
1955 let single_line = options.max_lines == 1;
1956 match options.overflow {
1957 TextOverflow::Ellipsis => Some(Self::End),
1958 TextOverflow::StartEllipsis if single_line => Some(Self::Start),
1959 TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
1960 TextOverflow::StartEllipsis
1961 | TextOverflow::MiddleEllipsis
1962 | TextOverflow::Clip
1963 | TextOverflow::Visible
1964 | TextOverflow::ScaleDown { .. } => None,
1965 }
1966 }
1967
1968 fn elide(
1969 self,
1970 source: &crate::text::AnnotatedString,
1971 source_range: Range<usize>,
1972 boundaries: &[usize],
1973 kept_chars: usize,
1974 ) -> crate::text::AnnotatedString {
1975 let char_count = boundaries.len() - 1;
1976 let (head_chars, tail_chars) = match self {
1977 Self::End => (kept_chars, 0),
1978 Self::Start => (0, kept_chars),
1979 Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
1980 };
1981 let head_end = source_range.start + boundaries[head_chars];
1982 let tail_start = source_range.start + boundaries[char_count - tail_chars];
1983 crate::text::AnnotatedString::builder()
1984 .append_annotated_subsequence(source, source_range.start..head_end)
1985 .append(ELLIPSIS)
1986 .append_annotated_subsequence(source, tail_start..source_range.end)
1987 .to_annotated_string()
1988 }
1989}
1990
1991fn fit_ellipsis<M: TextMeasurer + ?Sized>(
1992 measurer: &M,
1993 node_id: Option<NodeId>,
1994 source: &crate::text::AnnotatedString,
1995 source_range: Range<usize>,
1996 style: &TextStyle,
1997 max_width: Option<f32>,
1998 placement: EllipsisPlacement,
1999) -> DisplayLine {
2000 let width_limit = max_width.unwrap_or(f32::INFINITY);
2001 let fitting_line = |text: DisplayLineText| {
2002 let mut line = DisplayLine {
2003 source_range: source_range.clone(),
2004 text,
2005 measured_width: None,
2006 };
2007 let width = line.measure_width(measurer, node_id, source, style);
2008 line.measured_width = Some(width);
2009 (width <= width_limit + WRAP_EPSILON).then_some(line)
2010 };
2011 if placement != EllipsisPlacement::End
2012 && let Some(line) = fitting_line(DisplayLineText::Source)
2013 {
2014 return line;
2015 }
2016
2017 let boundaries = char_boundaries(&source.text[source_range.clone()]);
2018 let elided_line = |kept_chars: usize| {
2019 fitting_line(DisplayLineText::Ellipsized(placement.elide(
2020 source,
2021 source_range.clone(),
2022 &boundaries,
2023 kept_chars,
2024 )))
2025 };
2026 let Some(mut best) = elided_line(0) else {
2027 return DisplayLine {
2028 source_range: source_range.clone(),
2029 text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2030 measured_width: None,
2031 };
2032 };
2033
2034 let mut fitting = 0usize;
2035 let mut overflowing = boundaries.len();
2036 while fitting + 1 < overflowing {
2037 let kept_chars = fitting + (overflowing - fitting) / 2;
2038 match elided_line(kept_chars) {
2039 Some(line) => {
2040 fitting = kept_chars;
2041 best = line;
2042 }
2043 None => overflowing = kept_chars,
2044 }
2045 }
2046 best
2047}
2048
2049fn char_boundaries(text: &str) -> Vec<usize> {
2050 let mut out = Vec::with_capacity(text.chars().count() + 1);
2051 out.push(0);
2052 for (idx, _) in text.char_indices() {
2053 if idx != 0 {
2054 out.push(idx);
2055 }
2056 }
2057 out.push(text.len());
2058 out
2059}
2060
2061#[cfg(test)]
2062#[path = "tests/measure_tests.rs"]
2063mod tests;