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