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