1use std::{
2 borrow::Cow,
3 cell::{Cell, RefCell},
4 hash::Hash,
5 ops::Range,
6 rc::Rc,
7 sync::Arc,
8};
9
10use cranpose_core::{NodeId, collections::pass_aged::PassAgedCache};
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)]
41pub struct TextMetrics {
42 pub width: f32,
44 pub height: f32,
46 pub line_height: f32,
48 pub line_count: usize,
50}
51
52#[derive(Clone, Debug)]
55pub struct PreparedTextLayout {
56 pub text: Rc<crate::text::AnnotatedString>,
58 pub visual_style: std::sync::Arc<TextStyle>,
61 pub metrics: TextMetrics,
63 pub alignment_lines: cranpose_ui_layout::AlignmentLines,
65 pub did_overflow: bool,
68 pub render_text: std::cell::OnceCell<(std::sync::Arc<crate::text::RenderString>, u64)>,
72 pub(crate) visual_style_hash: std::cell::OnceCell<u64>,
73 pub(crate) wrap_hold: Option<WrapHold>,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq)]
83pub(crate) struct WrapHold {
84 fits: f32,
85 pulls_up: f32,
86}
87
88impl WrapHold {
89 const ANY: Self = Self {
91 fits: f32::NEG_INFINITY,
92 pulls_up: f32::INFINITY,
93 };
94
95 fn narrow(&mut self, fits: f32, pulls_up: f32) {
96 self.fits = self.fits.max(fits);
97 self.pulls_up = self.pulls_up.min(pulls_up);
98 }
99
100 fn holds(self, width: f32) -> bool {
101 self.fits <= width + WRAP_EPSILON && width + WRAP_EPSILON < self.pulls_up
102 }
103
104 fn fit_whole(hold: &mut Option<Self>, width: f32) {
106 if let Some(hold) = hold {
107 hold.narrow(width, f32::INFINITY);
108 }
109 }
110
111 fn settle(hold: Option<Self>, measured_width: f32, max_width: Option<f32>) -> Option<Self> {
115 let mut hold = hold?;
116 hold.narrow(measured_width + WRAP_EPSILON, f32::INFINITY);
117 max_width.filter(|width| hold.holds(*width)).map(|_| hold)
118 }
119}
120
121impl PreparedTextLayout {
122 pub fn render_text(&self) -> std::sync::Arc<crate::text::RenderString> {
126 if let Some((converted, _)) = self.render_text.get() {
127 return std::sync::Arc::clone(converted);
128 }
129 self.convert_render_text().0
130 }
131
132 pub fn render_text_hash(&self) -> u64 {
136 if let Some((_, hash)) = self.render_text.get() {
137 return *hash;
138 }
139 self.convert_render_text().1
140 }
141
142 fn convert_render_text(&self) -> (std::sync::Arc<crate::text::RenderString>, u64) {
143 let converted = std::sync::Arc::new(self.text.render_string());
144 let hash = converted.render_hash();
145 let _ = self
146 .render_text
147 .set((std::sync::Arc::clone(&converted), hash));
148 (converted, hash)
149 }
150
151 pub fn visual_style_hash(&self) -> u64 {
154 if let Some(hash) = self.visual_style_hash.get() {
155 return *hash;
156 }
157 let hash = self.visual_style.render_hash();
158 let _ = self.visual_style_hash.set(hash);
159 hash
160 }
161}
162
163impl PartialEq for PreparedTextLayout {
164 fn eq(&self, other: &Self) -> bool {
165 self.text == other.text
166 && self.visual_style == other.visual_style
167 && self.metrics == other.metrics
168 && self.alignment_lines == other.alignment_lines
169 && self.did_overflow == other.did_overflow
170 }
171}
172
173#[derive(Clone, Debug)]
177pub struct TextLinePrefixWidths {
178 table: Rc<PrefixWidthTable>,
179 first: usize,
181}
182
183#[derive(Debug)]
184struct PrefixWidthTable {
185 prefix_widths: Vec<f32>,
186 separator_before: Vec<f32>,
187 non_empty_overhang: f32,
188}
189
190impl PartialEq for TextLinePrefixWidths {
191 fn eq(&self, other: &Self) -> bool {
192 self.table.prefix_widths[self.first..] == other.table.prefix_widths[other.first..]
193 && self.table.separator_before[self.first..]
194 == other.table.separator_before[other.first..]
195 && self.table.non_empty_overhang == other.table.non_empty_overhang
196 }
197}
198
199impl TextLinePrefixWidths {
200 pub fn from_parts(
207 prefix_widths: Vec<f32>,
208 separator_before: Vec<f32>,
209 non_empty_overhang: f32,
210 ) -> Option<Self> {
211 if prefix_widths.is_empty() || prefix_widths.len() != separator_before.len() + 1 {
212 return None;
213 }
214 if prefix_widths
215 .iter()
216 .chain(separator_before.iter())
217 .any(|value| !value.is_finite())
218 {
219 return None;
220 }
221 let non_empty_overhang = non_empty_overhang.max(0.0);
222 if !non_empty_overhang.is_finite() {
223 return None;
224 }
225 Some(Self {
226 table: Rc::new(PrefixWidthTable {
227 prefix_widths,
228 separator_before,
229 non_empty_overhang,
230 }),
231 first: 0,
232 })
233 }
234
235 pub fn monospaced(char_count: usize, char_width: f32, letter_spacing: f32) -> Option<Self> {
239 if !char_width.is_finite() || !letter_spacing.is_finite() {
240 return None;
241 }
242 let char_width = char_width.max(0.0);
243 let letter_spacing = letter_spacing.max(0.0);
244 let mut prefix_widths = Vec::with_capacity(char_count + 1);
245 let mut separator_before = Vec::with_capacity(char_count);
246 let mut width = 0.0f32;
247 prefix_widths.push(width);
248 for _ in 0..char_count {
249 separator_before.push(0.0);
250 width += char_width + letter_spacing;
251 prefix_widths.push(width);
252 }
253 Self::from_parts(prefix_widths, separator_before, 0.0)
254 }
255
256 pub fn char_count(&self) -> usize {
258 self.table.separator_before.len() - self.first
259 }
260
261 fn suffix(&self, first: usize) -> Option<Self> {
264 (first <= self.char_count()).then(|| Self {
265 table: Rc::clone(&self.table),
266 first: self.first + first,
267 })
268 }
269
270 pub fn width_for_char_range(&self, start: usize, end: usize) -> Option<f32> {
274 if start > end || end > self.char_count() {
275 return None;
276 }
277 if start == end {
278 return Some(0.0);
279 }
280 let table = &*self.table;
281 let (start, end) = (self.first + start, self.first + end);
282 let separator = table.separator_before.get(start).copied().unwrap_or(0.0);
283 Some(
284 (table.prefix_widths[end] - table.prefix_widths[start] - separator).max(0.0)
285 + table.non_empty_overhang,
286 )
287 }
288}
289
290pub trait TextMeasurer: 'static {
297 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
299
300 fn begin_layout_pass(&self) {}
304
305 fn measure_for_node(
308 &self,
309 node_id: Option<NodeId>,
310 text: &crate::text::AnnotatedString,
311 style: &TextStyle,
312 ) -> TextMetrics {
313 let _ = node_id;
314 self.measure(text, style)
315 }
316
317 fn measure_subsequence(
319 &self,
320 text: &crate::text::AnnotatedString,
321 range: Range<usize>,
322 style: &TextStyle,
323 ) -> TextMetrics {
324 self.measure(&text.subsequence(range), style)
325 }
326
327 fn measure_subsequence_for_node(
330 &self,
331 node_id: Option<NodeId>,
332 text: &crate::text::AnnotatedString,
333 range: Range<usize>,
334 style: &TextStyle,
335 ) -> TextMetrics {
336 let _ = node_id;
337 self.measure_subsequence(text, range, style)
338 }
339
340 fn measure_line_prefix_widths(
342 &self,
343 text: &crate::text::AnnotatedString,
344 line_range: Range<usize>,
345 style: &TextStyle,
346 ) -> Option<TextLinePrefixWidths> {
347 let _ = text;
348 let _ = line_range;
349 let _ = style;
350 None
351 }
352
353 fn measure_line_width(
356 &self,
357 text: &crate::text::AnnotatedString,
358 line_range: Range<usize>,
359 style: &TextStyle,
360 ) -> Option<f32> {
361 let _ = text;
362 let _ = line_range;
363 let _ = style;
364 None
365 }
366
367 fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
369 self.measure(text, style).line_height
370 }
371
372 fn glyph_line_box(&self, style: &TextStyle) -> Option<(f32, f32)> {
381 let _ = style;
382 None
383 }
384
385 fn first_baseline(&self, style: &TextStyle) -> Option<f32> {
391 let _ = style;
392 None
393 }
394
395 fn line_box(&self, style: &TextStyle) -> Option<crate::text::LineBox> {
402 let _ = style;
403 None
404 }
405
406 fn visit_line_boxes(
411 &self,
412 text: &crate::text::AnnotatedString,
413 style: &TextStyle,
414 visit: &mut dyn FnMut(crate::text::LineBox),
415 ) -> Option<()> {
416 if !text.span_styles.is_empty() {
417 return None;
418 }
419 let line_box = self.line_box(style)?;
420 for _ in text.text.split('\n') {
421 visit(line_box);
422 }
423 Some(())
424 }
425
426 fn line_height_for_node(
429 &self,
430 node_id: Option<NodeId>,
431 text: &crate::text::AnnotatedString,
432 style: &TextStyle,
433 ) -> f32 {
434 let _ = node_id;
435 self.line_height(text, style)
436 }
437
438 fn line_box_and_height(
442 &self,
443 node_id: Option<NodeId>,
444 text: &crate::text::AnnotatedString,
445 style: &TextStyle,
446 ) -> (Option<crate::text::LineBox>, f32) {
447 (
448 self.line_box(style),
449 self.line_height_for_node(node_id, text, style),
450 )
451 }
452
453 fn get_offset_for_position(
456 &self,
457 text: &crate::text::AnnotatedString,
458 style: &TextStyle,
459 x: f32,
460 y: f32,
461 ) -> usize;
462
463 fn get_cursor_x_for_offset(
466 &self,
467 text: &crate::text::AnnotatedString,
468 style: &TextStyle,
469 offset: usize,
470 ) -> f32;
471
472 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
474
475 fn choose_auto_hyphen_break(
481 &self,
482 _line: &str,
483 _style: &TextStyle,
484 _segment_start_char: usize,
485 _measured_break_char: usize,
486 ) -> Option<usize> {
487 None
488 }
489
490 fn measure_with_options(
493 &self,
494 text: &crate::text::AnnotatedString,
495 style: &TextStyle,
496 options: TextLayoutOptions,
497 max_width: Option<f32>,
498 ) -> TextMetrics {
499 self.prepare_with_options(text, style, options, max_width)
500 .metrics
501 }
502
503 fn measure_with_options_for_node(
505 &self,
506 node_id: Option<NodeId>,
507 text: &crate::text::AnnotatedString,
508 style: &TextStyle,
509 options: TextLayoutOptions,
510 max_width: Option<f32>,
511 ) -> TextMetrics {
512 prepare_text_layout_with_measurer_for_node(self, node_id, text, style, options, max_width)
513 .metrics
514 }
515
516 fn prepare_with_options(
519 &self,
520 text: &crate::text::AnnotatedString,
521 style: &TextStyle,
522 options: TextLayoutOptions,
523 max_width: Option<f32>,
524 ) -> PreparedTextLayout {
525 self.prepare_with_options_fallback(text, style, options, max_width)
526 }
527
528 fn prepare_with_options_for_node(
531 &self,
532 node_id: Option<NodeId>,
533 text: &Rc<crate::text::AnnotatedString>,
534 style: &Arc<TextStyle>,
535 options: TextLayoutOptions,
536 max_width: Option<f32>,
537 ) -> PreparedTextLayout {
538 prepare_layout(
539 self,
540 node_id,
541 LayoutSource::Shared { text, style },
542 options,
543 max_width,
544 )
545 }
546
547 fn prepare_with_options_fallback(
551 &self,
552 text: &crate::text::AnnotatedString,
553 style: &TextStyle,
554 options: TextLayoutOptions,
555 max_width: Option<f32>,
556 ) -> PreparedTextLayout {
557 prepare_text_layout_fallback(self, text, style, options, max_width)
558 }
559}
560
561#[derive(Default)]
562struct MonospacedTextMeasurer;
563
564impl MonospacedTextMeasurer {
565 const DEFAULT_SIZE: f32 = 14.0;
566 const CHAR_WIDTH_RATIO: f32 = 0.6;
567
568 fn get_metrics(style: &TextStyle) -> (f32, f32) {
569 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
570 let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
571 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
572 (
573 (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
574 line_height,
575 )
576 }
577}
578
579impl TextMeasurer for MonospacedTextMeasurer {
580 fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
581 let (char_width, line_height) = Self::get_metrics(style);
582
583 let lines: Vec<&str> = text.text.split('\n').collect();
584 let line_count = lines.len().max(1);
585
586 let width = lines
587 .iter()
588 .map(|line| line.chars().count() as f32 * char_width)
589 .fold(0.0_f32, f32::max);
590
591 TextMetrics {
592 width,
593 height: line_count as f32 * line_height,
594 line_height,
595 line_count,
596 }
597 }
598
599 fn measure_subsequence(
600 &self,
601 text: &crate::text::AnnotatedString,
602 range: Range<usize>,
603 style: &TextStyle,
604 ) -> TextMetrics {
605 let (char_width, line_height) = Self::get_metrics(style);
606 let slice = &text.text[range];
607 let line_count = slice.split('\n').count().max(1);
608 let width = slice
609 .split('\n')
610 .map(|line| line.chars().count() as f32 * char_width)
611 .fold(0.0_f32, f32::max);
612
613 TextMetrics {
614 width,
615 height: line_count as f32 * line_height,
616 line_height,
617 line_count,
618 }
619 }
620
621 fn measure_line_prefix_widths(
622 &self,
623 text: &crate::text::AnnotatedString,
624 line_range: Range<usize>,
625 style: &TextStyle,
626 ) -> Option<TextLinePrefixWidths> {
627 let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
628 let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE);
629 TextLinePrefixWidths::monospaced(
630 text.text[line_range].chars().count(),
631 font_size * Self::CHAR_WIDTH_RATIO,
632 letter_spacing,
633 )
634 }
635
636 fn measure_line_width(
637 &self,
638 text: &crate::text::AnnotatedString,
639 line_range: Range<usize>,
640 style: &TextStyle,
641 ) -> Option<f32> {
642 Some(self.measure_subsequence(text, line_range, style).width)
643 }
644
645 fn line_height(&self, _text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
646 let (_, line_height) = Self::get_metrics(style);
647 line_height
648 }
649
650 fn get_offset_for_position(
651 &self,
652 text: &crate::text::AnnotatedString,
653 style: &TextStyle,
654 x: f32,
655 y: f32,
656 ) -> usize {
657 let (char_width, line_height) = Self::get_metrics(style);
658
659 if text.text.is_empty() {
660 return 0;
661 }
662
663 let line_index = (y / line_height).floor().max(0.0) as usize;
664 let lines: Vec<&str> = text.text.split('\n').collect();
665 let target_line = line_index.min(lines.len().saturating_sub(1));
666
667 let mut line_start_byte = 0;
668 for line in lines.iter().take(target_line) {
669 line_start_byte += line.len() + 1;
670 }
671
672 let line_text = lines.get(target_line).unwrap_or(&"");
673 let char_index = (x / char_width).round() as usize;
674 let line_char_count = line_text.chars().count();
675 let clamped_index = char_index.min(line_char_count);
676
677 let offset_in_line = line_text
678 .char_indices()
679 .nth(clamped_index)
680 .map_or(line_text.len(), |(i, _)| i);
681
682 line_start_byte + offset_in_line
683 }
684
685 fn get_cursor_x_for_offset(
686 &self,
687 text: &crate::text::AnnotatedString,
688 style: &TextStyle,
689 offset: usize,
690 ) -> f32 {
691 let (char_width, _) = Self::get_metrics(style);
692
693 let clamped_offset = offset.min(text.text.len());
694 let char_count = text.text[..clamped_offset].chars().count();
695 char_count as f32 * char_width
696 }
697
698 fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
699 let (char_width, line_height) = Self::get_metrics(style);
700 TextLayoutResult::monospaced(&text.text, char_width, line_height)
701 }
702}
703
704#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
705struct TextBaseCacheKey {
706 text_hash: u64,
707 style_hash: u64,
708}
709
710#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
711struct TextOptionsCacheKey {
712 base: TextBaseCacheKey,
713 options: TextLayoutOptions,
714 max_width_bits: Option<u32>,
715}
716
717#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
718struct TextPreparedCacheKey {
719 base: TextOptionsCacheKey,
720 visual_hash: u64,
721}
722
723pub(crate) struct TextService {
724 generation: Cell<u64>,
725 measurer: RefCell<Rc<dyn TextMeasurer>>,
726 metrics_cache: RefCell<PassAgedCache<TextBaseCacheKey, TextMetrics>>,
727 options_metrics_cache: RefCell<PassAgedCache<TextOptionsCacheKey, TextMetrics>>,
728 prepared_cache: RefCell<PassAgedCache<TextPreparedCacheKey, Rc<PreparedTextLayout>>>,
729 layout_cache: RefCell<PassAgedCache<TextBaseCacheKey, TextLayoutResult>>,
730}
731
732impl TextService {
733 pub(crate) fn new() -> Self {
734 Self::from_measurer(Rc::new(MonospacedTextMeasurer))
735 }
736
737 pub(crate) fn from_measurer(measurer: Rc<dyn TextMeasurer>) -> Self {
738 Self {
739 generation: Cell::new(1),
740 measurer: RefCell::new(measurer),
741 metrics_cache: RefCell::new(PassAgedCache::with_capacity_at_least_one(
742 TEXT_SERVICE_CACHE_CAPACITY,
743 )),
744 options_metrics_cache: RefCell::new(PassAgedCache::with_capacity_at_least_one(
745 TEXT_SERVICE_CACHE_CAPACITY,
746 )),
747 prepared_cache: RefCell::new(PassAgedCache::with_capacity_at_least_one(
748 TEXT_PREPARED_CACHE_CAPACITY,
749 )),
750 layout_cache: RefCell::new(PassAgedCache::with_capacity_at_least_one(
751 TEXT_SERVICE_CACHE_CAPACITY,
752 )),
753 }
754 }
755
756 pub(crate) fn set_measurer(&self, measurer: Rc<dyn TextMeasurer>) {
757 *self.measurer.borrow_mut() = measurer;
758 self.clear_caches();
759 }
760
761 pub(crate) fn generation(&self) -> u64 {
762 self.generation.get()
763 }
764
765 pub(crate) fn current_measurer(&self) -> Rc<dyn TextMeasurer> {
766 Rc::clone(&self.measurer.borrow())
767 }
768
769 pub(crate) fn with_measurer<R>(&self, f: impl FnOnce(&dyn TextMeasurer) -> R) -> R {
770 let measurer = self.current_measurer();
771 f(&*measurer)
772 }
773
774 pub(crate) fn measure(
775 &self,
776 node_id: Option<NodeId>,
777 text: &crate::text::AnnotatedString,
778 style: &TextStyle,
779 ) -> TextMetrics {
780 let key = text_base_cache_key(text, style);
781 if let Some(key) = key
782 && let Some(metrics) = self.metrics_cache.borrow_mut().get(&key).copied()
783 {
784 return metrics;
785 }
786 let metrics = self.with_measurer(|m| m.measure_for_node(node_id, text, style));
787 if let Some(key) = key {
788 self.metrics_cache.borrow_mut().push(key, metrics);
789 }
790 metrics
791 }
792
793 pub(crate) fn measure_with_options(
794 &self,
795 node_id: Option<NodeId>,
796 text: &crate::text::AnnotatedString,
797 style: &TextStyle,
798 options: TextLayoutOptions,
799 max_width: Option<f32>,
800 ) -> TextMetrics {
801 let key = text_options_cache_key(text, style, options.normalized(), max_width);
802 if let Some(key) = key {
803 if let Some(metrics) = self.options_metrics_cache.borrow_mut().get(&key).copied() {
804 return metrics;
805 }
806 let prepared_key = TextPreparedCacheKey {
807 base: key,
808 visual_hash: style.render_hash(),
809 };
810 if let Some(prepared) = self.prepared_cache.borrow_mut().get(&prepared_key) {
811 return prepared.metrics;
812 }
813 }
814 let metrics = self.with_measurer(|m| {
815 m.measure_with_options_for_node(node_id, text, style, options.normalized(), max_width)
816 });
817 if let Some(key) = key {
818 self.options_metrics_cache.borrow_mut().push(key, metrics);
819 }
820 metrics
821 }
822
823 pub(crate) fn prepare_with_options(
827 &self,
828 node_id: Option<NodeId>,
829 text: &Rc<crate::text::AnnotatedString>,
830 style: &Arc<TextStyle>,
831 options: TextLayoutOptions,
832 max_width: Option<f32>,
833 ) -> Rc<PreparedTextLayout> {
834 let key =
835 text_options_cache_key(text, style, options.normalized(), max_width).map(|base| {
836 TextPreparedCacheKey {
837 base,
838 visual_hash: style.render_hash(),
839 }
840 });
841 if let Some(key) = key
842 && let Some(prepared) = self.prepared_cache.borrow_mut().get(&key).map(Rc::clone)
843 {
844 return prepared;
845 }
846 let prepared = self.prepare_unshared(node_id, text, style, options, max_width);
847 if let Some(key) = key {
848 self.prepared_cache
849 .borrow_mut()
850 .push(key, Rc::clone(&prepared));
851 }
852 prepared
853 }
854
855 pub(crate) fn prepare_unshared(
856 &self,
857 node_id: Option<NodeId>,
858 text: &Rc<crate::text::AnnotatedString>,
859 style: &Arc<TextStyle>,
860 options: TextLayoutOptions,
861 max_width: Option<f32>,
862 ) -> Rc<PreparedTextLayout> {
863 Rc::new(self.with_measurer(|m| {
864 m.prepare_with_options_for_node(node_id, text, style, options.normalized(), max_width)
865 }))
866 }
867
868 pub(crate) fn layout(
869 &self,
870 text: &crate::text::AnnotatedString,
871 style: &TextStyle,
872 ) -> TextLayoutResult {
873 let key = text_base_cache_key(text, style);
874 if let Some(key) = key
875 && let Some(layout) = self.layout_cache.borrow_mut().get(&key).cloned()
876 {
877 return layout;
878 }
879 let layout = self.with_measurer(|m| m.layout(text, style));
880 if let Some(key) = key {
881 self.layout_cache.borrow_mut().push(key, layout.clone());
882 }
883 layout
884 }
885
886 pub(crate) fn begin_layout_pass(&self) {
889 self.with_measurer(TextMeasurer::begin_layout_pass);
890 self.metrics_cache.borrow_mut().begin_pass(drop);
891 self.options_metrics_cache.borrow_mut().begin_pass(drop);
892 self.prepared_cache.borrow_mut().begin_pass(drop);
893 self.layout_cache.borrow_mut().begin_pass(drop);
894 }
895
896 fn clear_caches(&self) {
897 self.generation
898 .set(self.generation.get().wrapping_add(1).max(1));
899 self.metrics_cache.borrow_mut().clear();
900 self.options_metrics_cache.borrow_mut().clear();
901 self.prepared_cache.borrow_mut().clear();
902 self.layout_cache.borrow_mut().clear();
903 }
904}
905
906fn text_base_cache_key(
907 text: &crate::text::AnnotatedString,
908 style: &TextStyle,
909) -> Option<TextBaseCacheKey> {
910 (text.string_annotations.is_empty() && text.link_annotations.is_empty()).then(|| {
911 TextBaseCacheKey {
912 text_hash: text.render_hash(),
913 style_hash: style.measurement_hash(),
914 }
915 })
916}
917
918fn text_options_cache_key(
919 text: &crate::text::AnnotatedString,
920 style: &TextStyle,
921 options: TextLayoutOptions,
922 max_width: Option<f32>,
923) -> Option<TextOptionsCacheKey> {
924 Some(TextOptionsCacheKey {
925 base: text_base_cache_key(text, style)?,
926 options: options.normalized(),
927 max_width_bits: normalize_max_width(max_width).map(f32::to_bits),
928 })
929}
930
931pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
934 crate::render_state::set_current_text_measurer(Rc::new(measurer));
935}
936
937pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
940 with_system_font_scale(text, style, |text, style| {
941 crate::render_state::with_text_service(|service| service.measure(None, text, style))
942 })
943}
944
945pub(crate) fn measure_resolved_text(
946 text: &crate::text::AnnotatedString,
947 style: &TextStyle,
948) -> TextMetrics {
949 crate::render_state::with_text_service(|service| service.measure(None, text, style))
950}
951
952pub(crate) fn resolved_first_baseline(style: &TextStyle) -> Option<f32> {
953 crate::render_state::with_text_service(|service| {
954 service.with_measurer(|m| m.first_baseline(style))
955 })
956}
957
958pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
959 crate::render_state::current_app_context()?;
960 crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(style)))
961}
962
963pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
967 let style = system_scaled_style(style);
968 crate::render_state::with_text_service(|service| {
969 service.with_measurer(|m| m.glyph_line_box(&style))
970 })
971 .map_or((0.0, line_height), |(off, h)| {
972 (off.min(line_height), h.min(line_height))
973 })
974}
975
976pub fn text_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
980 let style = system_scaled_style(style);
981 crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(&style)))
982}
983
984pub fn first_baseline(style: &TextStyle) -> Option<f32> {
988 let style = system_scaled_style(style);
989 crate::render_state::with_text_service(|service| {
990 service.with_measurer(|m| m.first_baseline(&style))
991 })
992}
993
994pub fn measure_text_for_node(
997 node_id: Option<NodeId>,
998 text: &crate::text::AnnotatedString,
999 style: &TextStyle,
1000) -> TextMetrics {
1001 with_system_font_scale(text, style, |text, style| {
1002 crate::render_state::with_text_service(|service| service.measure(node_id, text, style))
1003 })
1004}
1005
1006pub fn measure_text_with_options(
1009 text: &crate::text::AnnotatedString,
1010 style: &TextStyle,
1011 options: TextLayoutOptions,
1012 max_width: Option<f32>,
1013) -> TextMetrics {
1014 with_system_font_scale(text, style, |text, style| {
1015 crate::render_state::with_text_service(|service| {
1016 service.measure_with_options(None, text, style, options.normalized(), max_width)
1017 })
1018 })
1019}
1020
1021pub fn measure_text_with_options_for_node(
1024 node_id: Option<NodeId>,
1025 text: &crate::text::AnnotatedString,
1026 style: &TextStyle,
1027 options: TextLayoutOptions,
1028 max_width: Option<f32>,
1029) -> TextMetrics {
1030 with_system_font_scale(text, style, |text, style| {
1031 crate::render_state::with_text_service(|service| {
1032 service.measure_with_options(node_id, text, style, options.normalized(), max_width)
1033 })
1034 })
1035}
1036
1037pub fn prepare_text_layout(
1040 text: &crate::text::AnnotatedString,
1041 style: &TextStyle,
1042 options: TextLayoutOptions,
1043 max_width: Option<f32>,
1044) -> PreparedTextLayout {
1045 Rc::unwrap_or_clone(prepare_text_layout_for_node(
1046 None,
1047 &Rc::new(text.clone()),
1048 &Arc::new(style.clone()),
1049 options,
1050 max_width,
1051 ))
1052}
1053
1054pub fn prepare_text_layout_for_node(
1057 node_id: Option<NodeId>,
1058 text: &Rc<crate::text::AnnotatedString>,
1059 style: &Arc<TextStyle>,
1060 options: TextLayoutOptions,
1061 max_width: Option<f32>,
1062) -> Rc<PreparedTextLayout> {
1063 with_font_scaled(text, style, |text, style| {
1064 crate::render_state::with_text_service(|service| {
1065 service.prepare_with_options(node_id, text, style, options.normalized(), max_width)
1066 })
1067 })
1068}
1069
1070pub(crate) fn prepare_unshared_text_layout_for_node(
1071 node_id: Option<NodeId>,
1072 text: &Rc<crate::text::AnnotatedString>,
1073 style: &Arc<TextStyle>,
1074 options: TextLayoutOptions,
1075 max_width: Option<f32>,
1076) -> Rc<PreparedTextLayout> {
1077 with_font_scaled(text, style, |text, style| {
1078 crate::render_state::with_text_service(|service| {
1079 service.prepare_unshared(node_id, text, style, options, max_width)
1080 })
1081 })
1082}
1083
1084fn with_font_scaled<R>(
1085 text: &Rc<crate::text::AnnotatedString>,
1086 style: &Arc<TextStyle>,
1087 prepare: impl FnOnce(&Rc<crate::text::AnnotatedString>, &Arc<TextStyle>) -> R,
1088) -> R {
1089 let Some(curve) = crate::render_state::current_scaling_font_scale_curve() else {
1090 return prepare(text, style);
1091 };
1092 let scaled_text = match scale_annotated_font_sizes(text, curve) {
1093 Cow::Borrowed(_) => Rc::clone(text),
1094 Cow::Owned(scaled) => Rc::new(scaled),
1095 };
1096 prepare(
1097 &scaled_text,
1098 &Arc::new(scale_text_style_font_sizes(style, curve).into_owned()),
1099 )
1100}
1101
1102pub fn get_offset_for_position(
1106 text: &crate::text::AnnotatedString,
1107 style: &TextStyle,
1108 x: f32,
1109 y: f32,
1110) -> usize {
1111 with_system_font_scale(text, style, |text, style| {
1112 crate::render_state::with_text_measurer(|m| m.get_offset_for_position(text, style, x, y))
1113 })
1114}
1115
1116pub fn offset_for_position_wrapped(
1130 text: &str,
1131 style: &TextStyle,
1132 node_id: Option<NodeId>,
1133 wrap_width: Option<f32>,
1134 line_height: f32,
1135 x: f32,
1136 y: f32,
1137) -> usize {
1138 if text.is_empty() {
1139 return 0;
1140 }
1141 let annotated = crate::text::AnnotatedString::from(text);
1142 let line_ranges = wrapped_line_ranges(
1143 node_id,
1144 &annotated,
1145 style,
1146 TextLayoutOptions::default(),
1147 wrap_width,
1148 );
1149 if line_ranges.is_empty() {
1150 return 0;
1151 }
1152 let line_idx = if line_height > 0.0 {
1153 (y / line_height).floor().max(0.0) as usize
1154 } else {
1155 0
1156 }
1157 .min(line_ranges.len() - 1);
1158 let range = &line_ranges[line_idx];
1159 let line = &text[range.start..range.end];
1160 let within = get_offset_for_position(&crate::text::AnnotatedString::from(line), style, x, 0.0);
1161 range.start + within.min(line.len())
1162}
1163
1164pub fn get_cursor_x_for_offset(
1167 text: &crate::text::AnnotatedString,
1168 style: &TextStyle,
1169 offset: usize,
1170) -> f32 {
1171 with_system_font_scale(text, style, |text, style| {
1172 crate::render_state::with_text_measurer(|m| m.get_cursor_x_for_offset(text, style, offset))
1173 })
1174}
1175
1176pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
1178 with_system_font_scale(text, style, |text, style| {
1179 crate::render_state::with_text_service(|service| service.layout(text, style))
1180 })
1181}
1182
1183pub fn wrapped_line_ranges(
1196 node_id: Option<NodeId>,
1197 text: &crate::text::AnnotatedString,
1198 style: &TextStyle,
1199 options: TextLayoutOptions,
1200 max_width: Option<f32>,
1201) -> Vec<Range<usize>> {
1202 with_system_font_scale(text, style, |text, style| {
1203 crate::render_state::with_text_measurer(|m| {
1204 wrapped_line_ranges_with_measurer(m, node_id, text, style, options, max_width)
1205 })
1206 })
1207}
1208
1209fn wrapped_line_ranges_with_measurer<M: TextMeasurer + ?Sized>(
1210 measurer: &M,
1211 _node_id: Option<NodeId>,
1212 text: &crate::text::AnnotatedString,
1213 style: &TextStyle,
1214 options: TextLayoutOptions,
1215 max_width: Option<f32>,
1216) -> Vec<Range<usize>> {
1217 let opts = options.normalized();
1218 let max_width = normalize_max_width(max_width);
1219 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
1220 .then_some(max_width)
1221 .flatten();
1222 let line_break_mode = style
1223 .paragraph_style
1224 .line_break
1225 .take_or_else(|| LineBreak::Simple);
1226 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
1227
1228 let line_ranges = split_line_ranges(text.text.as_str());
1229 let Some(width_limit) = wrap_width else {
1230 return line_ranges.into_vec();
1231 };
1232 let mut lines = DisplayLines::with_capacity(line_ranges.len());
1233 for line_range in line_ranges {
1234 wrap_line_to_width(
1235 measurer,
1236 text,
1237 line_range,
1238 style,
1239 (width_limit, LineLimit::NONE, &mut None),
1240 (line_break_mode, hyphens_mode),
1241 &mut lines,
1242 );
1243 }
1244 lines.into_iter().map(|line| line.source_range).collect()
1245}
1246
1247pub(crate) fn min_intrinsic_text_width(
1251 text: &crate::text::AnnotatedString,
1252 style: &TextStyle,
1253) -> f32 {
1254 with_system_font_scale(text, style, |text, style| {
1255 crate::render_state::with_text_measurer(|measurer| {
1256 widest_unbreakable_width(measurer, text, style)
1257 })
1258 })
1259}
1260
1261fn widest_unbreakable_width<M: TextMeasurer + ?Sized>(
1262 measurer: &M,
1263 text: &crate::text::AnnotatedString,
1264 style: &TextStyle,
1265) -> f32 {
1266 let mut widest = 0.0_f32;
1267 for line_range in split_line_ranges(text.text.as_str()) {
1268 let line_text = &text.text[line_range.clone()];
1269 if line_text.is_empty() {
1270 continue;
1271 }
1272 let boundaries = cluster_boundaries(line_text);
1273 let end = boundaries.len() - 1;
1274 let context = LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1275 let breaks = LineBreaks::new(line_text, &boundaries);
1276 let mut start = 0;
1277 for idx in 1..=end {
1278 if idx < end && !breaks.before(idx) {
1279 continue;
1280 }
1281 let mut word_end = idx;
1282 while word_end > start && breaks.is_space(word_end - 1) {
1283 word_end -= 1;
1284 }
1285 if word_end > start {
1286 widest = widest.max(context.measure_char_range(&boundaries, start, word_end));
1287 }
1288 start = idx;
1289 }
1290 }
1291 widest
1292}
1293
1294fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
1295 measurer: &M,
1296 text: &crate::text::AnnotatedString,
1297 style: &TextStyle,
1298 options: TextLayoutOptions,
1299 max_width: Option<f32>,
1300) -> PreparedTextLayout {
1301 prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
1302}
1303
1304pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
1307 measurer: &M,
1308 node_id: Option<NodeId>,
1309 text: &crate::text::AnnotatedString,
1310 style: &TextStyle,
1311 options: TextLayoutOptions,
1312 max_width: Option<f32>,
1313) -> PreparedTextLayout {
1314 prepare_layout(
1315 measurer,
1316 node_id,
1317 LayoutSource::Borrowed { text, style },
1318 options,
1319 max_width,
1320 )
1321}
1322
1323#[derive(Clone, Copy)]
1326enum LayoutSource<'a> {
1327 Borrowed {
1328 text: &'a crate::text::AnnotatedString,
1329 style: &'a TextStyle,
1330 },
1331 Shared {
1332 text: &'a Rc<crate::text::AnnotatedString>,
1333 style: &'a Arc<TextStyle>,
1334 },
1335}
1336
1337impl LayoutSource<'_> {
1338 fn text(&self) -> &crate::text::AnnotatedString {
1339 match self {
1340 Self::Borrowed { text, .. } => text,
1341 Self::Shared { text, .. } => text,
1342 }
1343 }
1344
1345 fn style(&self) -> &TextStyle {
1346 match self {
1347 Self::Borrowed { style, .. } => style,
1348 Self::Shared { style, .. } => style,
1349 }
1350 }
1351
1352 fn display_text(
1355 &self,
1356 lines: &[DisplayLine],
1357 unchanged: bool,
1358 ) -> Rc<crate::text::AnnotatedString> {
1359 match self {
1360 Self::Shared { text, .. } if unchanged => Rc::clone(text),
1361 _ => Rc::new(build_display_annotated(self.text(), lines)),
1362 }
1363 }
1364
1365 fn visual_style(&self) -> Arc<TextStyle> {
1366 match self {
1367 Self::Borrowed { style, .. } => Arc::new((*style).clone()),
1368 Self::Shared { style, .. } => Arc::clone(style),
1369 }
1370 }
1371}
1372
1373fn prepare_layout<M: TextMeasurer + ?Sized>(
1374 measurer: &M,
1375 node_id: Option<NodeId>,
1376 source: LayoutSource<'_>,
1377 options: TextLayoutOptions,
1378 max_width: Option<f32>,
1379) -> PreparedTextLayout {
1380 let (text, style) = (source.text(), source.style());
1381 let telemetry = text_layout_telemetry_enabled();
1382 let total_start = telemetry.then(Instant::now);
1383 let opts = options.normalized();
1384 let max_width = normalize_max_width(max_width);
1385 if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
1386 return prepare_scale_down_text_layout(
1387 measurer,
1388 node_id,
1389 text,
1390 style,
1391 opts,
1392 max_width,
1393 min_font_size_sp,
1394 );
1395 }
1396
1397 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
1398 .then_some(max_width)
1399 .flatten();
1400 let line_break_mode = style
1401 .paragraph_style
1402 .line_break
1403 .take_or_else(|| LineBreak::Simple);
1404 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
1405
1406 let wrap_start = telemetry.then(Instant::now);
1407 let line_ranges = split_line_ranges(text.text.as_str());
1408 let source_line_count = line_ranges.len();
1409 let mut visible_lines: DisplayLines;
1410 let mut wrap_hold = None;
1411 let mut cut_line = None;
1412 if let Some(width_limit) = wrap_width {
1413 (visible_lines, wrap_hold, cut_line) = wrap_lines(
1414 measurer,
1415 text,
1416 line_ranges,
1417 style,
1418 (width_limit, LineLimit::of(opts)),
1419 (line_break_mode, hyphens_mode),
1420 );
1421 } else {
1422 visible_lines = line_ranges
1423 .into_iter()
1424 .map(DisplayLine::from_source_range)
1425 .collect();
1426 }
1427 let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1428
1429 let overflow_start = telemetry.then(Instant::now);
1430 let did_overflow = apply_overflow(
1431 measurer,
1432 node_id,
1433 (text, style),
1434 opts,
1435 max_width,
1436 (&mut visible_lines, &mut wrap_hold, cut_line.as_ref()),
1437 );
1438 let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1439
1440 let build_start = telemetry.then(Instant::now);
1441 let unchanged = !did_overflow
1443 && visible_lines.len() == source_line_count
1444 && visible_lines
1445 .iter()
1446 .all(|line| matches!(line.text, DisplayLineText::Source));
1447 let display_annotated = source.display_text(&visible_lines, unchanged);
1448 debug_assert_eq!(
1449 display_annotated.text,
1450 join_display_line_text(text, &visible_lines)
1451 );
1452 let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1453
1454 let metrics_start = telemetry.then(Instant::now);
1455 let display_line_count = visible_lines.len().max(1);
1456 let layout_line_count = display_line_count.max(opts.min_lines);
1457
1458 let measured_width = if visible_lines.is_empty() {
1459 0.0
1460 } else {
1461 visible_lines
1462 .iter_mut()
1463 .map(|line| line.measure_width(measurer, node_id, text, style))
1464 .fold(0.0_f32, f32::max)
1465 };
1466 let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1467 let width = if opts.overflow == TextOverflow::Visible {
1468 measured_width
1469 } else if let Some(width_limit) = max_width {
1470 measured_width.min(width_limit)
1471 } else {
1472 measured_width
1473 };
1474 let wrap_hold = WrapHold::settle(wrap_hold, measured_width, wrap_width);
1475
1476 let vertical = prepared_line_metrics(
1477 measurer,
1478 node_id,
1479 text,
1480 &display_annotated,
1481 style,
1482 opts.min_lines,
1483 );
1484 let prepared = PreparedTextLayout {
1485 text: display_annotated,
1486 visual_style: source.visual_style(),
1487 alignment_lines: vertical.alignment_lines,
1488 metrics: TextMetrics {
1489 width,
1490 height: vertical.height,
1491 line_height: vertical.line_height,
1492 line_count: layout_line_count,
1493 },
1494 did_overflow,
1495 render_text: Default::default(),
1496 visual_style_hash: Default::default(),
1497 wrap_hold,
1498 };
1499
1500 if let Some(start) = total_start {
1501 eprintln!(
1502 "[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}",
1503 text.text.len(),
1504 text.span_styles.len(),
1505 source_line_count,
1506 display_line_count,
1507 wrap_width.is_some(),
1508 max_width,
1509 wrap_ms.unwrap_or(0.0),
1510 overflow_ms.unwrap_or(0.0),
1511 build_ms.unwrap_or(0.0),
1512 metrics_ms.unwrap_or(0.0),
1513 start.elapsed().as_secs_f64() * 1000.0,
1514 );
1515 }
1516
1517 prepared
1518}
1519
1520struct PreparedLineMetrics {
1521 height: f32,
1522 line_height: f32,
1523 alignment_lines: cranpose_ui_layout::AlignmentLines,
1524}
1525
1526fn prepared_line_metrics<M: TextMeasurer + ?Sized>(
1527 measurer: &M,
1528 node_id: Option<NodeId>,
1529 source: &crate::text::AnnotatedString,
1530 display: &crate::text::AnnotatedString,
1531 style: &TextStyle,
1532 min_lines: usize,
1533) -> PreparedLineMetrics {
1534 let mut span_base_box = None;
1535 if !display.span_styles.is_empty() {
1536 let base_box = measurer.line_box(style);
1537 span_base_box = Some(base_box);
1538 let mut top = 0.0;
1539 let mut trim_bottom = 0.0;
1540 let mut first = None;
1541 let mut last = None;
1542 let mut line_height = 0.0_f32;
1543 let resolved = measurer.visit_line_boxes(display, style, &mut |line| {
1544 if first.is_none() {
1545 top = -line.trim_top;
1546 first = Some(top + line.baseline);
1547 }
1548 last = Some(top + line.baseline);
1549 top += line.height;
1550 trim_bottom = line.trim_bottom;
1551 line_height = line_height.max(line.height);
1552 });
1553 if resolved.is_some() {
1554 let min_height = if min_lines > 1 {
1555 base_box.map_or(0.0, |line| line.block_height(min_lines))
1556 } else {
1557 0.0
1558 };
1559 return PreparedLineMetrics {
1560 height: (top - trim_bottom).max(min_height),
1561 line_height,
1562 alignment_lines: cranpose_ui_layout::AlignmentLines::new(first, last),
1563 };
1564 }
1565 }
1566 let measured_text = if source.span_styles.is_empty() {
1567 source
1568 } else {
1569 display
1570 };
1571 let (base_box, line_height) = match span_base_box {
1572 Some(base_box) => (
1573 base_box,
1574 measurer.line_height_for_node(node_id, measured_text, style),
1575 ),
1576 None => measurer.line_box_and_height(node_id, measured_text, style),
1577 };
1578 let line_height = line_height.max(0.0);
1579 let first = base_box
1580 .map(crate::text::LineBox::first_baseline)
1581 .or_else(|| measurer.first_baseline(style));
1582 let displayed_lines = display.text.split('\n').count().max(1);
1583 let layout_line_count = displayed_lines.max(min_lines);
1584 let edges = base_box.unwrap_or_else(|| crate::text::LineBox::untrimmed(line_height, 0.0));
1585 PreparedLineMetrics {
1586 height: (layout_line_count as f32 * line_height - edges.trim_top - edges.trim_bottom)
1587 .max(0.0),
1588 line_height,
1589 alignment_lines: cranpose_ui_layout::AlignmentLines::new(
1590 first,
1591 first.map(|first| first + (displayed_lines - 1) as f32 * line_height),
1592 ),
1593 }
1594}
1595
1596fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1597 measurer: &M,
1598 node_id: Option<NodeId>,
1599 text: &crate::text::AnnotatedString,
1600 style: &TextStyle,
1601 options: TextLayoutOptions,
1602 max_width: Option<f32>,
1603 min_font_size_sp: f32,
1604) -> PreparedTextLayout {
1605 let clipped_options = TextLayoutOptions {
1606 overflow: TextOverflow::Clip,
1607 ..options
1608 }
1609 .normalized();
1610
1611 let full_size = prepare_scaled_text_layout(
1612 measurer,
1613 node_id,
1614 text,
1615 style,
1616 clipped_options,
1617 max_width,
1618 FontScaleCurve::linear(1.0),
1619 );
1620 let Some(width_limit) = max_width else {
1621 return full_size;
1622 };
1623 if !full_size.did_overflow {
1624 return full_size;
1625 }
1626
1627 let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1628 if !base_font_size.is_finite() || base_font_size <= 0.0 {
1629 return full_size;
1630 }
1631 let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1632 if min_scale >= 1.0 {
1633 return full_size;
1634 }
1635
1636 let min_size = prepare_scaled_text_layout(
1637 measurer,
1638 node_id,
1639 text,
1640 style,
1641 clipped_options,
1642 Some(width_limit),
1643 FontScaleCurve::linear(min_scale),
1644 );
1645 if min_size.did_overflow {
1646 return min_size;
1647 }
1648
1649 let mut low = min_scale;
1650 let mut high = 1.0;
1651 let mut best = min_size;
1652 for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1653 let mid = (low + high) * 0.5;
1654 let candidate = prepare_scaled_text_layout(
1655 measurer,
1656 node_id,
1657 text,
1658 style,
1659 clipped_options,
1660 Some(width_limit),
1661 FontScaleCurve::linear(mid),
1662 );
1663 if candidate.did_overflow {
1664 high = mid;
1665 } else {
1666 low = mid;
1667 best = candidate;
1668 }
1669 }
1670
1671 best
1672}
1673
1674fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1675 measurer: &M,
1676 node_id: Option<NodeId>,
1677 text: &crate::text::AnnotatedString,
1678 style: &TextStyle,
1679 options: TextLayoutOptions,
1680 max_width: Option<f32>,
1681 shrink: FontScaleCurve,
1682) -> PreparedTextLayout {
1683 let visual_style = scale_text_style_font_sizes(style, shrink);
1684 let visual_text = scale_annotated_font_sizes(text, shrink);
1685 prepare_text_layout_with_measurer_for_node(
1686 measurer,
1687 node_id,
1688 visual_text.as_ref(),
1689 &visual_style,
1690 options,
1691 max_width,
1692 )
1693}
1694
1695fn scale_annotated_font_sizes(
1696 text: &crate::text::AnnotatedString,
1697 curve: FontScaleCurve,
1698) -> Cow<'_, crate::text::AnnotatedString> {
1699 if curve.is_identity() || !annotated_text_needs_scaling(text) {
1700 return Cow::Borrowed(text);
1701 }
1702
1703 let mut scaled = text.clone();
1704 for span in &mut scaled.span_styles {
1705 scale_span_style_font_sizes(&mut span.item, curve, None);
1706 }
1707 Cow::Owned(scaled)
1708}
1709
1710fn scale_text_style_font_sizes(style: &TextStyle, curve: FontScaleCurve) -> Cow<'_, TextStyle> {
1711 if curve.is_identity() {
1712 return Cow::Borrowed(style);
1713 }
1714
1715 let mut scaled = style.clone();
1716 scale_span_style_font_sizes(&mut scaled.span_style, curve, Some(DEFAULT_FONT_SIZE_SP));
1717 scaled.paragraph_style.line_height =
1718 scale_text_unit_sp(scaled.paragraph_style.line_height, curve);
1719 if let Some(mut indent) = scaled.paragraph_style.text_indent {
1720 indent.first_line = scale_text_unit_sp(indent.first_line, curve);
1721 indent.rest_line = scale_text_unit_sp(indent.rest_line, curve);
1722 scaled.paragraph_style.text_indent = Some(indent);
1723 }
1724 Cow::Owned(scaled)
1725}
1726
1727fn system_scaled_style(style: &TextStyle) -> Cow<'_, TextStyle> {
1729 match crate::render_state::current_scaling_font_scale_curve() {
1730 Some(curve) => scale_text_style_font_sizes(style, curve),
1731 None => Cow::Borrowed(style),
1732 }
1733}
1734
1735fn with_system_font_scale<R>(
1736 text: &crate::text::AnnotatedString,
1737 style: &TextStyle,
1738 block: impl FnOnce(&crate::text::AnnotatedString, &TextStyle) -> R,
1739) -> R {
1740 let Some(curve) = crate::render_state::current_scaling_font_scale_curve() else {
1741 return block(text, style);
1742 };
1743 let visual_style = scale_text_style_font_sizes(style, curve);
1744 let visual_text = scale_annotated_font_sizes(text, curve);
1745 block(visual_text.as_ref(), &visual_style)
1746}
1747
1748fn scale_span_style_font_sizes(
1750 scaled: &mut crate::text::SpanStyle,
1751 curve: FontScaleCurve,
1752 default_font_size_sp: Option<f32>,
1753) {
1754 let factor = curve.scale();
1755 scaled.font_size = match (scaled.font_size, default_font_size_sp) {
1756 (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1757 crate::text::TextUnit::Sp(curve.sp_to_dp(default_size))
1758 }
1759 (unit, Some(_)) => scale_text_unit_sp_and_em(unit, curve),
1760 (unit, None) => scale_text_unit_sp(unit, curve),
1761 };
1762 scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, curve);
1763 if let Some(mut shadow) = scaled.shadow {
1764 shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1765 shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1766 shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1767 scaled.shadow = Some(shadow);
1768 }
1769 if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1770 scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1771 width: width * factor,
1772 });
1773 }
1774}
1775
1776fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1777 text.span_styles
1778 .iter()
1779 .any(|span| span_style_needs_scaling(&span.item))
1780}
1781
1782fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1783 matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1784 || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1785 || matches!(
1786 style.draw_style,
1787 Some(crate::text::TextDrawStyle::Stroke { .. })
1788 )
1789 || style.shadow.is_some()
1790}
1791
1792fn scale_text_unit_sp(unit: crate::text::TextUnit, curve: FontScaleCurve) -> crate::text::TextUnit {
1793 match unit {
1794 crate::text::TextUnit::Sp(value) if value.is_finite() => {
1795 crate::text::TextUnit::Sp(curve.sp_to_dp(value))
1796 }
1797 other => other,
1798 }
1799}
1800
1801fn scale_text_unit_sp_and_em(
1802 unit: crate::text::TextUnit,
1803 curve: FontScaleCurve,
1804) -> crate::text::TextUnit {
1805 match unit {
1806 crate::text::TextUnit::Sp(_) => scale_text_unit_sp(unit, curve),
1807 crate::text::TextUnit::Em(value) if value.is_finite() => {
1808 crate::text::TextUnit::Em(value * curve.scale())
1809 }
1810 other => other,
1811 }
1812}
1813
1814fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1815 if value.is_finite() {
1816 value * factor
1817 } else {
1818 value
1819 }
1820}
1821
1822#[derive(Clone, Debug)]
1823enum DisplayLineText {
1824 Source,
1825 Ellipsized(crate::text::AnnotatedString),
1826}
1827
1828#[derive(Clone, Debug)]
1829struct DisplayLine {
1830 source_range: Range<usize>,
1831 text: DisplayLineText,
1832 measured_width: Option<f32>,
1833}
1834
1835impl DisplayLine {
1836 fn from_source_range(source_range: Range<usize>) -> Self {
1837 Self {
1838 source_range,
1839 text: DisplayLineText::Source,
1840 measured_width: None,
1841 }
1842 }
1843
1844 fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1845 Self {
1846 source_range,
1847 text: DisplayLineText::Source,
1848 measured_width: measured_width
1849 .is_finite()
1850 .then_some(measured_width.max(0.0)),
1851 }
1852 }
1853
1854 fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1855 match &self.text {
1856 DisplayLineText::Source => &source.text[self.source_range.clone()],
1857 DisplayLineText::Ellipsized(annotated) => annotated.text.as_str(),
1858 }
1859 }
1860
1861 fn measure_width<M: TextMeasurer + ?Sized>(
1863 &mut self,
1864 measurer: &M,
1865 node_id: Option<NodeId>,
1866 source: &crate::text::AnnotatedString,
1867 style: &TextStyle,
1868 ) -> f32 {
1869 *self.measured_width.get_or_insert_with(|| match &self.text {
1870 DisplayLineText::Source => {
1871 measurer
1872 .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1873 .width
1874 }
1875 DisplayLineText::Ellipsized(annotated) => {
1876 measurer.measure_for_node(node_id, annotated, style).width
1877 }
1878 })
1879 }
1880
1881 fn extend_to_paragraph_end(&mut self, source: &crate::text::AnnotatedString) {
1882 let start = self.source_range.start;
1883 let end = source.text[start..]
1884 .find('\n')
1885 .map_or(source.text.len(), |offset| start + offset);
1886 self.source_range = start..end;
1887 self.text = DisplayLineText::Source;
1888 self.measured_width = None;
1889 }
1890
1891 fn ellipsize<M: TextMeasurer + ?Sized>(
1894 &mut self,
1895 measurer: &M,
1896 node_id: Option<NodeId>,
1897 (source, style): (&crate::text::AnnotatedString, &TextStyle),
1898 (max_width, placement): (Option<f32>, EllipsisPlacement),
1899 cut_line: Option<&MeasuredSourceLine>,
1900 ) -> WrapHold {
1901 let (line, cut) = fit_ellipsis(
1902 measurer,
1903 node_id,
1904 (source, self.source_range.clone()),
1905 style,
1906 (max_width, placement),
1907 cut_line,
1908 );
1909 *self = line;
1910 cut
1911 }
1912}
1913
1914type LineRanges = smallvec::SmallVec<[Range<usize>; 1]>;
1916type DisplayLines = smallvec::SmallVec<[DisplayLine; 1]>;
1918
1919fn split_line_ranges(text: &str) -> LineRanges {
1920 if text.is_empty() {
1921 return smallvec::smallvec![0..0];
1922 }
1923
1924 let mut ranges = LineRanges::new();
1927 let mut start = 0usize;
1928 for (idx, byte) in text.bytes().enumerate() {
1929 if byte == b'\n' {
1930 ranges.push(start..idx);
1931 start = idx + 1;
1932 }
1933 }
1934 ranges.push(start..text.len());
1935 ranges
1936}
1937
1938fn build_display_annotated(
1939 source: &crate::text::AnnotatedString,
1940 lines: &[DisplayLine],
1941) -> crate::text::AnnotatedString {
1942 if lines.is_empty() {
1943 return crate::text::AnnotatedString::from("");
1944 }
1945
1946 let mut builder = crate::text::AnnotatedString::builder();
1947 for (idx, line) in lines.iter().enumerate() {
1948 builder = match &line.text {
1949 DisplayLineText::Source => {
1950 builder.append_annotated_subsequence(source, line.source_range.clone())
1951 }
1952 DisplayLineText::Ellipsized(annotated) => builder.append_annotated(annotated),
1953 };
1954 if idx + 1 < lines.len() {
1955 builder = builder.append("\n");
1956 }
1957 }
1958 builder.to_annotated_string()
1959}
1960
1961fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1962 let mut text = String::new();
1963 for (idx, line) in lines.iter().enumerate() {
1964 text.push_str(line.display_text(source));
1965 if idx + 1 < lines.len() {
1966 text.push('\n');
1967 }
1968 }
1969 text
1970}
1971
1972fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1973 while end > start {
1974 let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1975 break;
1976 };
1977 if ch.is_whitespace() {
1978 end = start + idx;
1979 } else {
1980 break;
1981 }
1982 }
1983 end
1984}
1985
1986#[derive(Clone, Copy, Debug, PartialEq)]
1989pub(crate) enum PreparedWidths {
1990 Exact(Option<u32>),
1992 AtLeast(f32),
1995 Wrapped(WrapHold),
1998}
1999
2000impl PreparedWidths {
2001 pub(crate) fn of(
2003 text: &crate::text::AnnotatedString,
2004 options: TextLayoutOptions,
2005 max_width: Option<f32>,
2006 prepared: &PreparedTextLayout,
2007 ) -> Self {
2008 let max_width = normalize_max_width(max_width);
2009 let exact = Self::Exact(max_width.map(f32::to_bits));
2010 let wrapped = prepared.text.text.matches('\n').count() != text.text.matches('\n').count();
2011 let trailing_space = text
2014 .text
2015 .split('\n')
2016 .any(|line| line.ends_with(char::is_whitespace));
2017 if options
2018 .normalized()
2019 .overflow
2020 .scale_down_min_font_size_sp()
2021 .is_some()
2022 || trailing_space
2023 {
2024 return exact;
2025 }
2026 if wrapped || prepared.did_overflow {
2027 return match (prepared.wrap_hold, max_width) {
2028 (Some(hold), Some(width)) if hold.holds(width) => Self::Wrapped(hold),
2029 _ => exact,
2030 };
2031 }
2032 match max_width {
2033 Some(width) if prepared.metrics.width >= width => exact,
2034 _ => Self::AtLeast(prepared.metrics.width),
2035 }
2036 }
2037
2038 pub(crate) fn max_width_range(self) -> cranpose_ui_layout::BoundRange {
2043 match self {
2044 Self::Exact(None) => cranpose_ui_layout::BoundRange::exactly(f32::INFINITY),
2045 Self::Exact(Some(bits)) => {
2046 cranpose_ui_layout::BoundRange::exactly(f32::from_bits(bits))
2047 }
2048 Self::AtLeast(min) => cranpose_ui_layout::BoundRange::from(min.next_up()),
2049 Self::Wrapped(hold) => cranpose_ui_layout::BoundRange {
2050 low: (hold.fits - WRAP_EPSILON).next_up(),
2051 high: (hold.pulls_up - WRAP_EPSILON).next_down().next_down(),
2052 },
2053 }
2054 }
2055
2056 pub(crate) fn hold(self, max_width: Option<f32>) -> bool {
2058 let max_width = normalize_max_width(max_width);
2059 match self {
2060 Self::Exact(bits) => max_width.map(f32::to_bits) == bits,
2061 Self::AtLeast(min) => max_width.is_none_or(|width| width >= min),
2062 Self::Wrapped(hold) => max_width.is_some_and(|width| hold.holds(width)),
2063 }
2064 }
2065}
2066
2067fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
2068 match max_width {
2069 Some(width) if width.is_finite() && width > 0.0 => Some(width),
2070 _ => None,
2071 }
2072}
2073
2074fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
2075 (base_start + relative.start)..(base_start + relative.end)
2076}
2077
2078fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
2079 boundaries
2080 .binary_search(&byte_offset)
2081 .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
2082}
2083
2084struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
2085 measurer: &'a M,
2086 text: &'a crate::text::AnnotatedString,
2087 style: &'a TextStyle,
2088 line_start: usize,
2089 prefix_widths: Option<TextLinePrefixWidths>,
2090}
2091
2092impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
2093 fn new(
2094 measurer: &'a M,
2095 text: &'a crate::text::AnnotatedString,
2096 line_range: &Range<usize>,
2097 style: &'a TextStyle,
2098 boundary_count: usize,
2099 ) -> Self {
2100 let expected_chars = boundary_count.saturating_sub(1);
2101 let prefix_widths = measurer
2102 .measure_line_prefix_widths(text, line_range.clone(), style)
2103 .filter(|widths| widths.char_count() == expected_chars);
2104 Self {
2105 measurer,
2106 text,
2107 style,
2108 line_start: line_range.start,
2109 prefix_widths,
2110 }
2111 }
2112
2113 fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
2114 if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
2115 return width;
2116 }
2117 let segment_range =
2118 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
2119 self.measurer
2120 .measure_subsequence(self.text, segment_range, self.style)
2121 .width
2122 }
2123
2124 fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
2125 if let Some(prefix_widths) = &self.prefix_widths
2126 && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
2127 {
2128 return Some(width);
2129 }
2130 None
2131 }
2132
2133 fn display_line_for_char_range(
2134 &self,
2135 boundaries: &[usize],
2136 start_idx: usize,
2137 end_idx: usize,
2138 ) -> DisplayLine {
2139 let source_range =
2140 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
2141 let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
2142 DisplayLine::from_measured_source_range(source_range, measured_width)
2143 }
2144}
2145
2146#[derive(Clone, Copy, Debug)]
2151struct LineLimit {
2152 lines: usize,
2153 elides_last: bool,
2154}
2155
2156impl LineLimit {
2157 const NONE: Self = Self {
2158 lines: usize::MAX,
2159 elides_last: false,
2160 };
2161
2162 fn of(options: TextLayoutOptions) -> Self {
2163 Self {
2164 lines: options.max_lines,
2165 elides_last: EllipsisPlacement::for_options(options).is_some(),
2166 }
2167 }
2168
2169 fn reached(self, lines: usize) -> bool {
2172 lines >= self.lines
2173 }
2174
2175 fn elides_next(self, lines: usize) -> bool {
2178 self.elides_last && lines + 1 == self.lines
2179 }
2180}
2181
2182fn wrap_lines<M: TextMeasurer + ?Sized>(
2183 measurer: &M,
2184 text: &crate::text::AnnotatedString,
2185 line_ranges: LineRanges,
2186 style: &TextStyle,
2187 (max_width, limit): (f32, LineLimit),
2188 modes: (LineBreak, Hyphens),
2189) -> (DisplayLines, Option<WrapHold>, Option<MeasuredSourceLine>) {
2190 let source_lines = line_ranges.len();
2191 let mut lines = DisplayLines::with_capacity(source_lines.min(limit.lines.saturating_add(1)));
2192 let mut hold = Some(WrapHold::ANY);
2193 let mut cut_line = None;
2194 for line_range in line_ranges {
2195 if limit.reached(lines.len()) {
2196 if lines.len() == limit.lines {
2197 lines.push(DisplayLine::from_source_range(line_range));
2198 }
2199 break;
2200 }
2201 cut_line = wrap_line_to_width(
2202 measurer,
2203 text,
2204 line_range,
2205 style,
2206 (max_width, limit, &mut hold),
2207 modes,
2208 &mut lines,
2209 );
2210 }
2211 let wrapped = lines.len() != source_lines;
2212 (lines, hold.filter(|_| wrapped), cut_line)
2213}
2214
2215fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
2220 measurer: &M,
2221 text: &crate::text::AnnotatedString,
2222 line_range: Range<usize>,
2223 style: &TextStyle,
2224 (max_width, limit, hold): (f32, LineLimit, &mut Option<WrapHold>),
2225 (line_break, hyphens): (LineBreak, Hyphens),
2226 out: &mut DisplayLines,
2227) -> Option<MeasuredSourceLine> {
2228 let line_text = &text.text[line_range.clone()];
2229 if line_text.is_empty() {
2230 out.push(DisplayLine::from_source_range(
2231 line_range.start..line_range.start,
2232 ));
2233 return None;
2234 }
2235
2236 if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
2237 && measured_width <= max_width + WRAP_EPSILON
2238 {
2239 WrapHold::fit_whole(hold, measured_width);
2240 out.push(DisplayLine::from_measured_source_range(
2241 line_range,
2242 measured_width,
2243 ));
2244 return None;
2245 }
2246
2247 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
2248 && line_text.chars().any(char::is_whitespace)
2249 && wrap_line_with_word_balance(
2250 measurer,
2251 text,
2252 line_range.clone(),
2253 style,
2254 max_width,
2255 line_break,
2256 out,
2257 )
2258 {
2259 *hold = None;
2260 return None;
2261 }
2262
2263 wrap_line_greedy(
2264 measurer,
2265 text,
2266 line_range,
2267 style,
2268 (max_width, limit, hold),
2269 (line_break, hyphens),
2270 out,
2271 )
2272}
2273
2274fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
2275 measurer: &M,
2276 text: &crate::text::AnnotatedString,
2277 line_range: Range<usize>,
2278 style: &TextStyle,
2279 (max_width, limit, hold): (f32, LineLimit, &mut Option<WrapHold>),
2280 (line_break, hyphens): (LineBreak, Hyphens),
2281 out: &mut DisplayLines,
2282) -> Option<MeasuredSourceLine> {
2283 let line_text = &text.text[line_range.clone()];
2284 let boundaries = cluster_boundaries(line_text);
2285 let mut measure_context =
2286 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
2287 if let Some(measured_width) =
2288 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
2289 && measured_width <= max_width + WRAP_EPSILON
2290 {
2291 WrapHold::fit_whole(hold, measured_width);
2292 out.push(DisplayLine::from_measured_source_range(
2293 line_range,
2294 measured_width,
2295 ));
2296 return None;
2297 }
2298 let first = out.len();
2299 let breaks = LineBreaks::new(line_text, &boundaries);
2300 let mut start_idx = 0usize;
2301
2302 while start_idx < boundaries.len() - 1 {
2303 if limit.reached(out.len()) {
2304 out.push(DisplayLine::from_source_range(
2305 line_range.start + boundaries[start_idx]..line_range.end,
2306 ));
2307 return Some(MeasuredSourceLine {
2310 start: line_range.start,
2311 prefix_widths: measure_context.prefix_widths.take(),
2312 boundaries,
2313 });
2314 }
2315 let mut low = start_idx + 1;
2316 let mut high = boundaries.len() - 1;
2317 let mut best = start_idx + 1;
2318
2319 while low <= high {
2320 let mid = (low + high) / 2;
2321 let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
2322 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
2323 best = mid;
2324 low = mid + 1;
2325 } else {
2326 if mid == 0 {
2327 break;
2328 }
2329 high = mid - 1;
2330 }
2331 }
2332
2333 let wrap_idx = choose_wrap_break(&breaks, start_idx, best, line_break);
2334 let mut effective_wrap_idx = wrap_idx;
2335 let can_hyphenate = hyphens == Hyphens::Auto
2336 && wrap_idx == best
2337 && best < boundaries.len() - 1
2338 && is_break_inside_word(&breaks, wrap_idx);
2339 narrow_to_break(
2340 hold,
2341 &measure_context,
2342 &breaks,
2343 (start_idx, best),
2344 (can_hyphenate, limit.elides_next(out.len())),
2345 );
2346 if can_hyphenate {
2347 effective_wrap_idx =
2348 resolve_auto_hyphen_break(measurer, style, &breaks, start_idx, wrap_idx);
2349 }
2350
2351 let broke_at_word_boundary = breaks.ends_at_space(start_idx, effective_wrap_idx);
2352 let segment_start = boundaries[start_idx];
2353 let mut segment_end = boundaries[effective_wrap_idx];
2354 if wrap_idx != best || broke_at_word_boundary {
2355 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
2356 }
2357 let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
2358 out.push(measure_context.display_line_for_char_range(
2359 &boundaries,
2360 start_idx,
2361 segment_end_idx,
2362 ));
2363
2364 start_idx = if wrap_idx != best || broke_at_word_boundary {
2365 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
2366 } else {
2367 effective_wrap_idx
2368 };
2369 }
2370
2371 if out.len() == first {
2372 out.push(DisplayLine::from_source_range(
2373 line_range.start..line_range.start,
2374 ));
2375 }
2376 None
2377}
2378
2379struct MeasuredSourceLine {
2382 start: usize,
2383 boundaries: Vec<usize>,
2384 prefix_widths: Option<TextLinePrefixWidths>,
2385}
2386
2387impl MeasuredSourceLine {
2388 fn suffix(&self, range: &Range<usize>) -> Option<(Vec<usize>, Option<TextLinePrefixWidths>)> {
2393 let offset = range.start.checked_sub(self.start)?;
2394 if self.start + *self.boundaries.last()? != range.end {
2395 return None;
2396 }
2397 let first = self.boundaries.binary_search(&offset).ok()?;
2398 let boundaries = self.boundaries[first..]
2399 .iter()
2400 .map(|boundary| boundary - offset)
2401 .collect();
2402 let widths = self
2403 .prefix_widths
2404 .as_ref()
2405 .and_then(|widths| widths.suffix(first));
2406 Some((boundaries, widths))
2407 }
2408}
2409
2410fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
2413 measurer: &M,
2414 text: &crate::text::AnnotatedString,
2415 line_range: Range<usize>,
2416 style: &TextStyle,
2417 max_width: f32,
2418 line_break: LineBreak,
2419 out: &mut DisplayLines,
2420) -> bool {
2421 let line_text = &text.text[line_range.clone()];
2422 let boundaries = cluster_boundaries(line_text);
2423 let measure_context =
2424 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
2425 if let Some(measured_width) =
2426 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
2427 && measured_width <= max_width + WRAP_EPSILON
2428 {
2429 out.push(DisplayLine::from_measured_source_range(
2430 line_range,
2431 measured_width,
2432 ));
2433 return true;
2434 }
2435 let breakpoints = collect_word_breakpoints(&LineBreaks::new(line_text, &boundaries));
2436 if breakpoints.len() <= 2 {
2437 return false;
2438 }
2439
2440 let node_count = breakpoints.len();
2441 let mut best_cost = vec![f32::INFINITY; node_count];
2442 let mut next_index = vec![None; node_count];
2443 best_cost[node_count - 1] = 0.0;
2444
2445 for start in (0..node_count - 1).rev() {
2446 for end in start + 1..node_count {
2447 let start_byte = boundaries[breakpoints[start]];
2448 let end_byte = boundaries[breakpoints[end]];
2449 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2450 if trimmed_end <= start_byte {
2451 continue;
2452 }
2453 let segment_start_idx = breakpoints[start];
2454 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2455 let segment_width =
2456 measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
2457 if segment_width > max_width + WRAP_EPSILON {
2458 continue;
2459 }
2460 if !best_cost[end].is_finite() {
2461 continue;
2462 }
2463 let slack = (max_width - segment_width).max(0.0);
2464 let is_last = end == node_count - 1;
2465 let segment_cost = match line_break {
2466 LineBreak::Heading => slack * slack,
2467 LineBreak::Paragraph => {
2468 if is_last {
2469 slack * slack * 0.16
2470 } else {
2471 slack * slack
2472 }
2473 }
2474 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
2475 };
2476 let candidate = segment_cost + best_cost[end];
2477 if candidate < best_cost[start] {
2478 best_cost[start] = candidate;
2479 next_index[start] = Some(end);
2480 }
2481 }
2482 }
2483
2484 let first = out.len();
2485 let mut current = 0usize;
2486 while current < node_count - 1 {
2487 let Some(next) = next_index[current] else {
2488 out.truncate(first);
2489 return false;
2490 };
2491 let start_byte = boundaries[breakpoints[current]];
2492 let end_byte = boundaries[breakpoints[next]];
2493 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2494 if trimmed_end <= start_byte {
2495 out.truncate(first);
2496 return false;
2497 }
2498 let segment_start_idx = breakpoints[current];
2499 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2500 out.push(measure_context.display_line_for_char_range(
2501 &boundaries,
2502 segment_start_idx,
2503 segment_end_idx,
2504 ));
2505 current = next;
2506 }
2507 out.len() > first
2508}
2509
2510struct LineBreaks<'a> {
2513 line: &'a str,
2514 boundaries: &'a [usize],
2515 words: Vec<usize>,
2518}
2519
2520impl<'a> LineBreaks<'a> {
2521 fn new(line: &'a str, boundaries: &'a [usize]) -> Self {
2522 Self {
2523 line,
2524 boundaries,
2525 words: super::word_breaks::unspaced_word_breaks(line, boundaries),
2526 }
2527 }
2528
2529 fn is_space(&self, idx: usize) -> bool {
2531 self.line[self.boundaries[idx]..self.boundaries[idx + 1]]
2532 .chars()
2533 .all(char::is_whitespace)
2534 }
2535
2536 fn ends_at_space(&self, start: usize, end: usize) -> bool {
2540 (end > start && self.is_space(end - 1))
2541 || (end < self.boundaries.len() - 1 && self.is_space(end))
2542 }
2543
2544 fn before(&self, idx: usize) -> bool {
2547 self.is_space(idx - 1) || self.words.binary_search(&idx).is_ok()
2548 }
2549}
2550
2551fn collect_word_breakpoints(breaks: &LineBreaks<'_>) -> Vec<usize> {
2552 let boundaries = breaks.boundaries;
2553 let mut points = vec![0usize];
2554 for idx in 1..boundaries.len() - 1 {
2555 if breaks.before(idx) && !breaks.is_space(idx) {
2556 points.push(idx);
2557 }
2558 }
2559 let end = boundaries.len() - 1;
2560 if points.last().copied() != Some(end) {
2561 points.push(end);
2562 }
2563 points
2564}
2565
2566fn choose_wrap_break(
2567 breaks: &LineBreaks<'_>,
2568 start_idx: usize,
2569 best: usize,
2570 _line_break: LineBreak,
2571) -> usize {
2572 if best >= breaks.boundaries.len() - 1 {
2573 return best;
2574 }
2575
2576 if best <= start_idx + 1 {
2577 return best;
2578 }
2579
2580 (start_idx + 1..=best)
2581 .rev()
2582 .find(|&idx| breaks.before(idx))
2583 .unwrap_or(best)
2584}
2585
2586fn narrow_to_break<M: TextMeasurer + ?Sized>(
2593 hold: &mut Option<WrapHold>,
2594 measure_context: &LineMeasureContext<'_, M>,
2595 breaks: &LineBreaks<'_>,
2596 (start_idx, best): (usize, usize),
2597 (hyphenated, elided): (bool, bool),
2598) {
2599 let boundaries = breaks.boundaries;
2600 let end = boundaries.len() - 1;
2601 if elided && best < end {
2602 if let Some(hold) = hold {
2603 let rest = measure_context.measure_char_range(boundaries, start_idx, end);
2604 hold.narrow(f32::NEG_INFINITY, rest);
2605 }
2606 return;
2607 }
2608 if hyphenated {
2609 *hold = None;
2610 }
2611 let Some(hold) = hold else {
2612 return;
2613 };
2614 let (fits, pulls_up) = wrap_break_widths(breaks, start_idx, best);
2615 let width = |idx| measure_context.measure_char_range(boundaries, start_idx, idx);
2616 hold.narrow(
2617 fits.map_or(f32::NEG_INFINITY, width),
2618 pulls_up.map_or(f32::INFINITY, width),
2619 );
2620}
2621
2622fn wrap_break_widths(
2628 breaks: &LineBreaks<'_>,
2629 start_idx: usize,
2630 best: usize,
2631) -> (Option<usize>, Option<usize>) {
2632 let end = breaks.boundaries.len() - 1;
2633 if best <= start_idx + 1 {
2635 return (None, (best < end).then_some(best + 1));
2636 }
2637 if best >= end {
2638 return (Some(end), None);
2639 }
2640 match (start_idx + 1..=best).rev().find(|&idx| breaks.before(idx)) {
2641 Some(wrap_idx) => {
2642 let next = (best + 1..end)
2643 .find(|&idx| breaks.before(idx))
2644 .unwrap_or(end);
2645 (Some(wrap_idx), Some(next))
2646 }
2647 None => (Some(best), Some(best + 1)),
2648 }
2649}
2650
2651fn is_break_inside_word(breaks: &LineBreaks<'_>, break_idx: usize) -> bool {
2652 if break_idx == 0 || break_idx >= breaks.boundaries.len() - 1 {
2653 return false;
2654 }
2655 !breaks.before(break_idx) && !breaks.is_space(break_idx)
2656}
2657
2658fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
2659 measurer: &M,
2660 style: &TextStyle,
2661 breaks: &LineBreaks<'_>,
2662 start_idx: usize,
2663 break_idx: usize,
2664) -> usize {
2665 let boundaries = breaks.boundaries;
2666 if let Some(candidate) =
2667 measurer.choose_auto_hyphen_break(breaks.line, style, start_idx, break_idx)
2668 && is_valid_auto_hyphen_break(breaks, start_idx, break_idx, candidate)
2669 {
2670 return candidate;
2671 }
2672 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
2673}
2674
2675fn is_valid_auto_hyphen_break(
2676 breaks: &LineBreaks<'_>,
2677 start_idx: usize,
2678 break_idx: usize,
2679 candidate_idx: usize,
2680) -> bool {
2681 let end_idx = breaks.boundaries.len().saturating_sub(1);
2682 candidate_idx > start_idx
2683 && candidate_idx < end_idx
2684 && candidate_idx <= break_idx
2685 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
2686 && is_break_inside_word(breaks, candidate_idx)
2687}
2688
2689fn choose_auto_hyphen_break_fallback(
2690 boundaries: &[usize],
2691 start_idx: usize,
2692 break_idx: usize,
2693) -> usize {
2694 let end_idx = boundaries.len().saturating_sub(1);
2695 if break_idx >= end_idx {
2696 return break_idx;
2697 }
2698 let trailing_len = end_idx.saturating_sub(break_idx);
2699 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
2700 return break_idx;
2701 }
2702
2703 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
2704 let max_break = break_idx.saturating_sub(1);
2705 if min_break > max_break {
2706 return break_idx;
2707 }
2708
2709 let mut best_break = break_idx;
2710 let mut best_penalty = usize::MAX;
2711 for idx in min_break..=max_break {
2712 let candidate_trailing_len = end_idx.saturating_sub(idx);
2713 let candidate_prefix_len = idx.saturating_sub(start_idx);
2714 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
2715 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
2716 {
2717 continue;
2718 }
2719
2720 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
2721 if penalty < best_penalty {
2722 best_penalty = penalty;
2723 best_break = idx;
2724 if penalty == 0 {
2725 break;
2726 }
2727 }
2728 }
2729 best_break
2730}
2731
2732fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
2733 while idx < boundaries.len() - 1 {
2734 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
2735 if !ch.chars().all(char::is_whitespace) {
2736 break;
2737 }
2738 idx += 1;
2739 }
2740 idx
2741}
2742
2743fn apply_overflow<M: TextMeasurer + ?Sized>(
2748 measurer: &M,
2749 node_id: Option<NodeId>,
2750 (text, style): (&crate::text::AnnotatedString, &TextStyle),
2751 options: TextLayoutOptions,
2752 max_width: Option<f32>,
2753 (visible_lines, hold, cut_line): (
2754 &mut DisplayLines,
2755 &mut Option<WrapHold>,
2756 Option<&MeasuredSourceLine>,
2757 ),
2758) -> bool {
2759 if options.overflow == TextOverflow::Visible {
2760 return false;
2761 }
2762 let ellipsis = EllipsisPlacement::for_options(options);
2763 let mut did_overflow = false;
2764 if visible_lines.len() > options.max_lines {
2765 did_overflow = true;
2766 visible_lines.truncate(options.max_lines);
2767 if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
2768 last_line.extend_to_paragraph_end(text);
2769 let cut = last_line.ellipsize(
2770 measurer,
2771 node_id,
2772 (text, style),
2773 (max_width, placement),
2774 cut_line,
2775 );
2776 if let Some(hold) = hold {
2777 hold.narrow(cut.fits, cut.pulls_up);
2778 }
2779 }
2780 }
2781
2782 let Some(width_limit) = max_width else {
2783 return did_overflow;
2784 };
2785 let visible_len = visible_lines.len();
2786 for (line_index, line) in visible_lines.iter_mut().enumerate() {
2787 if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2788 continue;
2789 }
2790 did_overflow = true;
2791 *hold = None;
2792 if line_index + 1 == visible_len
2793 && let Some(placement) = ellipsis
2794 {
2795 line.ellipsize(
2796 measurer,
2797 node_id,
2798 (text, style),
2799 (max_width, placement),
2800 cut_line,
2801 );
2802 }
2803 }
2804 did_overflow
2805}
2806
2807#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2808enum EllipsisPlacement {
2809 End,
2810 Start,
2811 Middle,
2812}
2813
2814impl EllipsisPlacement {
2815 fn split(self, kept_chars: usize) -> (usize, usize) {
2817 match self {
2818 Self::End => (kept_chars, 0),
2819 Self::Start => (0, kept_chars),
2820 Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2821 }
2822 }
2823
2824 fn estimated_kept_chars(
2829 self,
2830 prefix_widths: &TextLinePrefixWidths,
2831 ellipsis_width: f32,
2832 width_limit: f32,
2833 ) -> Option<usize> {
2834 let char_count = prefix_widths.char_count();
2835 let width = |kept_chars: usize| {
2836 let (head_chars, tail_chars) = self.split(kept_chars);
2837 Some(
2838 prefix_widths.width_for_char_range(0, head_chars)?
2839 + ellipsis_width
2840 + prefix_widths.width_for_char_range(char_count - tail_chars, char_count)?,
2841 )
2842 };
2843 let (mut fitting, mut overflowing) = (0usize, char_count + 1);
2844 while fitting + 1 < overflowing {
2845 let kept_chars = fitting + (overflowing - fitting) / 2;
2846 if width(kept_chars)? <= width_limit + WRAP_EPSILON {
2847 fitting = kept_chars;
2848 } else {
2849 overflowing = kept_chars;
2850 }
2851 }
2852 Some(fitting)
2853 }
2854
2855 fn for_options(options: TextLayoutOptions) -> Option<Self> {
2856 let single_line = options.max_lines == 1;
2857 match options.overflow {
2858 TextOverflow::Ellipsis => Some(Self::End),
2859 TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2860 TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2861 TextOverflow::StartEllipsis
2862 | TextOverflow::MiddleEllipsis
2863 | TextOverflow::Clip
2864 | TextOverflow::Visible
2865 | TextOverflow::ScaleDown { .. } => None,
2866 }
2867 }
2868
2869 fn elide(
2870 self,
2871 source: &crate::text::AnnotatedString,
2872 source_range: Range<usize>,
2873 boundaries: &[usize],
2874 kept_chars: usize,
2875 ) -> crate::text::AnnotatedString {
2876 let char_count = boundaries.len() - 1;
2877 let (head_chars, tail_chars) = self.split(kept_chars);
2878 let head_end = source_range.start + boundaries[head_chars];
2879 let tail_start = source_range.start + boundaries[char_count - tail_chars];
2880 crate::text::AnnotatedString::builder()
2881 .append_annotated_subsequence(source, source_range.start..head_end)
2882 .append(ELLIPSIS)
2883 .append_annotated_subsequence(source, tail_start..source_range.end)
2884 .to_annotated_string()
2885 }
2886}
2887
2888fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2892 measurer: &M,
2893 node_id: Option<NodeId>,
2894 (source, source_range): (&crate::text::AnnotatedString, Range<usize>),
2895 style: &TextStyle,
2896 (max_width, placement): (Option<f32>, EllipsisPlacement),
2897 cut_line: Option<&MeasuredSourceLine>,
2898) -> (DisplayLine, WrapHold) {
2899 let width_limit = max_width.unwrap_or(f32::INFINITY);
2900 let fitting_line = |text: DisplayLineText| {
2902 let mut line = DisplayLine {
2903 source_range: source_range.clone(),
2904 text,
2905 measured_width: None,
2906 };
2907 let width = line.measure_width(measurer, node_id, source, style);
2908 if width <= width_limit + WRAP_EPSILON {
2909 Ok(line)
2910 } else {
2911 Err(width)
2912 }
2913 };
2914 let whole = if placement == EllipsisPlacement::End {
2917 f32::INFINITY
2918 } else {
2919 match fitting_line(DisplayLineText::Source) {
2920 Ok(line) => return ElisionSearch::found(line, f32::INFINITY),
2921 Err(width) => width,
2922 }
2923 };
2924
2925 let (boundaries, known_widths) = elided_line_measures(source, &source_range, cut_line);
2926 let elided_line = |kept_chars: usize| {
2927 fitting_line(DisplayLineText::Ellipsized(placement.elide(
2928 source,
2929 source_range.clone(),
2930 &boundaries,
2931 kept_chars,
2932 )))
2933 };
2934 let best = match elided_line(0) {
2935 Ok(line) => line,
2936 Err(ellipsis_width) => {
2937 let empty = DisplayLine {
2938 source_range: source_range.clone(),
2939 text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2940 measured_width: None,
2941 };
2942 return ElisionSearch::found(empty, ellipsis_width);
2943 }
2944 };
2945
2946 let guess = best.measured_width.and_then(|ellipsis_width| {
2950 known_widths
2951 .or_else(|| measurer.measure_line_prefix_widths(source, source_range.clone(), style))
2952 .filter(|widths| widths.char_count() + 1 == boundaries.len())
2953 .and_then(|widths| placement.estimated_kept_chars(&widths, ellipsis_width, width_limit))
2954 });
2955 let mut search = ElisionSearch {
2956 best,
2957 fitting: 0,
2958 overflowing: boundaries.len(),
2959 overflow_width: whole,
2960 };
2961 if let Some(guess) = guess.filter(|guess| *guess > 0) {
2962 for kept_chars in [guess, guess + 1] {
2963 if kept_chars <= search.fitting || kept_chars >= search.overflowing {
2964 break;
2965 }
2966 search.probe(kept_chars, elided_line(kept_chars));
2967 }
2968 }
2969 while search.fitting + 1 < search.overflowing {
2970 let kept_chars = search.fitting + (search.overflowing - search.fitting) / 2;
2971 search.probe(kept_chars, elided_line(kept_chars));
2972 }
2973 ElisionSearch::found(search.best, search.overflow_width)
2974}
2975
2976fn elided_line_measures(
2981 source: &crate::text::AnnotatedString,
2982 range: &Range<usize>,
2983 cut_line: Option<&MeasuredSourceLine>,
2984) -> (Vec<usize>, Option<TextLinePrefixWidths>) {
2985 cut_line
2986 .and_then(|line| line.suffix(range))
2987 .unwrap_or_else(|| (cluster_boundaries(&source.text[range.clone()]), None))
2988}
2989
2990struct ElisionSearch {
2993 best: DisplayLine,
2994 fitting: usize,
2995 overflowing: usize,
2996 overflow_width: f32,
2997}
2998
2999impl ElisionSearch {
3000 fn probe(&mut self, kept_chars: usize, line: Result<DisplayLine, f32>) {
3001 match line {
3002 Ok(line) => (self.fitting, self.best) = (kept_chars, line),
3003 Err(width) => (self.overflowing, self.overflow_width) = (kept_chars, width),
3004 }
3005 }
3006
3007 fn found(line: DisplayLine, overflow_width: f32) -> (DisplayLine, WrapHold) {
3010 let cut = WrapHold {
3011 fits: line.measured_width.unwrap_or(f32::NEG_INFINITY),
3012 pulls_up: overflow_width,
3013 };
3014 (line, cut)
3015 }
3016}
3017
3018fn cluster_boundaries(text: &str) -> Vec<usize> {
3022 let mut out = Vec::with_capacity(text.chars().count() + 1);
3023 out.push(0);
3024 for (idx, ch) in text.char_indices() {
3025 if idx != 0 && !is_combining_mark(ch) {
3026 out.push(idx);
3027 }
3028 }
3029 out.push(text.len());
3030 out
3031}
3032
3033fn is_combining_mark(ch: char) -> bool {
3035 use icu_properties::{
3036 CodePointMapData,
3037 props::{GeneralCategory, GeneralCategoryGroup},
3038 };
3039 ch >= '\u{300}'
3041 && GeneralCategoryGroup::Mark.contains(CodePointMapData::<GeneralCategory>::new().get(ch))
3042}
3043
3044#[cfg(test)]
3045#[path = "tests/measure_tests.rs"]
3046mod tests;