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