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 = char_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 mut start_idx = 0usize;
2147
2148 while start_idx < boundaries.len() - 1 {
2149 if limit.reached(out.len()) {
2150 out.push(DisplayLine::from_source_range(
2151 line_range.start + boundaries[start_idx]..line_range.end,
2152 ));
2153 return;
2154 }
2155 let mut low = start_idx + 1;
2156 let mut high = boundaries.len() - 1;
2157 let mut best = start_idx + 1;
2158
2159 while low <= high {
2160 let mid = (low + high) / 2;
2161 let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
2162 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
2163 best = mid;
2164 low = mid + 1;
2165 } else {
2166 if mid == 0 {
2167 break;
2168 }
2169 high = mid - 1;
2170 }
2171 }
2172
2173 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
2174 let mut effective_wrap_idx = wrap_idx;
2175 let can_hyphenate = hyphens == Hyphens::Auto
2176 && wrap_idx == best
2177 && best < boundaries.len() - 1
2178 && is_break_inside_word(line_text, &boundaries, wrap_idx);
2179 narrow_to_break(
2180 hold,
2181 &measure_context,
2182 (line_text, &boundaries),
2183 (start_idx, best),
2184 (can_hyphenate, limit.elides_next(out.len())),
2185 );
2186 if can_hyphenate {
2187 effective_wrap_idx = resolve_auto_hyphen_break(
2188 measurer,
2189 line_text,
2190 style,
2191 &boundaries,
2192 start_idx,
2193 wrap_idx,
2194 );
2195 }
2196
2197 let broke_at_word_boundary = effective_wrap_idx > start_idx
2198 && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
2199 .chars()
2200 .all(char::is_whitespace);
2201 let segment_start = boundaries[start_idx];
2202 let mut segment_end = boundaries[effective_wrap_idx];
2203 if wrap_idx != best || broke_at_word_boundary {
2204 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
2205 }
2206 let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
2207 out.push(measure_context.display_line_for_char_range(
2208 &boundaries,
2209 start_idx,
2210 segment_end_idx,
2211 ));
2212
2213 start_idx = if wrap_idx != best || broke_at_word_boundary {
2214 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
2215 } else {
2216 effective_wrap_idx
2217 };
2218 }
2219
2220 if out.len() == first {
2221 out.push(DisplayLine::from_source_range(
2222 line_range.start..line_range.start,
2223 ));
2224 }
2225}
2226
2227fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
2230 measurer: &M,
2231 text: &crate::text::AnnotatedString,
2232 line_range: Range<usize>,
2233 style: &TextStyle,
2234 max_width: f32,
2235 line_break: LineBreak,
2236 out: &mut DisplayLines,
2237) -> bool {
2238 let line_text = &text.text[line_range.clone()];
2239 let boundaries = char_boundaries(line_text);
2240 let measure_context =
2241 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
2242 if let Some(measured_width) =
2243 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
2244 && measured_width <= max_width + WRAP_EPSILON
2245 {
2246 out.push(DisplayLine::from_measured_source_range(
2247 line_range,
2248 measured_width,
2249 ));
2250 return true;
2251 }
2252 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
2253 if breakpoints.len() <= 2 {
2254 return false;
2255 }
2256
2257 let node_count = breakpoints.len();
2258 let mut best_cost = vec![f32::INFINITY; node_count];
2259 let mut next_index = vec![None; node_count];
2260 best_cost[node_count - 1] = 0.0;
2261
2262 for start in (0..node_count - 1).rev() {
2263 for end in start + 1..node_count {
2264 let start_byte = boundaries[breakpoints[start]];
2265 let end_byte = boundaries[breakpoints[end]];
2266 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2267 if trimmed_end <= start_byte {
2268 continue;
2269 }
2270 let segment_start_idx = breakpoints[start];
2271 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2272 let segment_width =
2273 measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
2274 if segment_width > max_width + WRAP_EPSILON {
2275 continue;
2276 }
2277 if !best_cost[end].is_finite() {
2278 continue;
2279 }
2280 let slack = (max_width - segment_width).max(0.0);
2281 let is_last = end == node_count - 1;
2282 let segment_cost = match line_break {
2283 LineBreak::Heading => slack * slack,
2284 LineBreak::Paragraph => {
2285 if is_last {
2286 slack * slack * 0.16
2287 } else {
2288 slack * slack
2289 }
2290 }
2291 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
2292 };
2293 let candidate = segment_cost + best_cost[end];
2294 if candidate < best_cost[start] {
2295 best_cost[start] = candidate;
2296 next_index[start] = Some(end);
2297 }
2298 }
2299 }
2300
2301 let first = out.len();
2302 let mut current = 0usize;
2303 while current < node_count - 1 {
2304 let Some(next) = next_index[current] else {
2305 out.truncate(first);
2306 return false;
2307 };
2308 let start_byte = boundaries[breakpoints[current]];
2309 let end_byte = boundaries[breakpoints[next]];
2310 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2311 if trimmed_end <= start_byte {
2312 out.truncate(first);
2313 return false;
2314 }
2315 let segment_start_idx = breakpoints[current];
2316 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2317 out.push(measure_context.display_line_for_char_range(
2318 &boundaries,
2319 segment_start_idx,
2320 segment_end_idx,
2321 ));
2322 current = next;
2323 }
2324 out.len() > first
2325}
2326
2327fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
2328 let mut points = vec![0usize];
2329 for idx in 1..boundaries.len() - 1 {
2330 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
2331 let current = &line[boundaries[idx]..boundaries[idx + 1]];
2332 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
2333 points.push(idx);
2334 }
2335 }
2336 let end = boundaries.len() - 1;
2337 if points.last().copied() != Some(end) {
2338 points.push(end);
2339 }
2340 points
2341}
2342
2343fn choose_wrap_break(
2344 line: &str,
2345 boundaries: &[usize],
2346 start_idx: usize,
2347 best: usize,
2348 _line_break: LineBreak,
2349) -> usize {
2350 if best >= boundaries.len() - 1 {
2351 return best;
2352 }
2353
2354 if best <= start_idx + 1 {
2355 return best;
2356 }
2357
2358 for idx in (start_idx + 1..=best).rev() {
2359 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
2360 if prev.chars().all(char::is_whitespace) {
2361 return idx;
2362 }
2363 }
2364 best
2365}
2366
2367fn narrow_to_break<M: TextMeasurer + ?Sized>(
2374 hold: &mut Option<WrapHold>,
2375 measure_context: &LineMeasureContext<'_, M>,
2376 (line, boundaries): (&str, &[usize]),
2377 (start_idx, best): (usize, usize),
2378 (hyphenated, elided): (bool, bool),
2379) {
2380 let end = boundaries.len() - 1;
2381 if elided && best < end {
2382 if let Some(hold) = hold {
2383 let rest = measure_context.measure_char_range(boundaries, start_idx, end);
2384 hold.narrow(f32::NEG_INFINITY, rest);
2385 }
2386 return;
2387 }
2388 if hyphenated {
2389 *hold = None;
2390 }
2391 let Some(hold) = hold else {
2392 return;
2393 };
2394 let (fits, pulls_up) = wrap_break_widths(line, boundaries, start_idx, best);
2395 let width = |idx| measure_context.measure_char_range(boundaries, start_idx, idx);
2396 hold.narrow(
2397 fits.map_or(f32::NEG_INFINITY, width),
2398 pulls_up.map_or(f32::INFINITY, width),
2399 );
2400}
2401
2402fn wrap_break_widths(
2408 line: &str,
2409 boundaries: &[usize],
2410 start_idx: usize,
2411 best: usize,
2412) -> (Option<usize>, Option<usize>) {
2413 let end = boundaries.len() - 1;
2414 if best <= start_idx + 1 {
2416 return (None, (best < end).then_some(best + 1));
2417 }
2418 if best >= end {
2419 return (Some(end), None);
2420 }
2421 let after_space = |idx: usize| {
2422 line[boundaries[idx - 1]..boundaries[idx]]
2423 .chars()
2424 .all(char::is_whitespace)
2425 };
2426 match (start_idx + 1..=best).rev().find(|&idx| after_space(idx)) {
2427 Some(wrap_idx) => {
2428 let next = (best + 1..end).find(|&idx| after_space(idx)).unwrap_or(end);
2429 (Some(wrap_idx), Some(next))
2430 }
2431 None => (Some(best), Some(best + 1)),
2432 }
2433}
2434
2435fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
2436 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
2437 return false;
2438 }
2439 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
2440 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
2441 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
2442}
2443
2444fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
2445 measurer: &M,
2446 line: &str,
2447 style: &TextStyle,
2448 boundaries: &[usize],
2449 start_idx: usize,
2450 break_idx: usize,
2451) -> usize {
2452 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
2453 && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
2454 {
2455 return candidate;
2456 }
2457 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
2458}
2459
2460fn is_valid_auto_hyphen_break(
2461 line: &str,
2462 boundaries: &[usize],
2463 start_idx: usize,
2464 break_idx: usize,
2465 candidate_idx: usize,
2466) -> bool {
2467 let end_idx = boundaries.len().saturating_sub(1);
2468 candidate_idx > start_idx
2469 && candidate_idx < end_idx
2470 && candidate_idx <= break_idx
2471 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
2472 && is_break_inside_word(line, boundaries, candidate_idx)
2473}
2474
2475fn choose_auto_hyphen_break_fallback(
2476 boundaries: &[usize],
2477 start_idx: usize,
2478 break_idx: usize,
2479) -> usize {
2480 let end_idx = boundaries.len().saturating_sub(1);
2481 if break_idx >= end_idx {
2482 return break_idx;
2483 }
2484 let trailing_len = end_idx.saturating_sub(break_idx);
2485 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
2486 return break_idx;
2487 }
2488
2489 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
2490 let max_break = break_idx.saturating_sub(1);
2491 if min_break > max_break {
2492 return break_idx;
2493 }
2494
2495 let mut best_break = break_idx;
2496 let mut best_penalty = usize::MAX;
2497 for idx in min_break..=max_break {
2498 let candidate_trailing_len = end_idx.saturating_sub(idx);
2499 let candidate_prefix_len = idx.saturating_sub(start_idx);
2500 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
2501 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
2502 {
2503 continue;
2504 }
2505
2506 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
2507 if penalty < best_penalty {
2508 best_penalty = penalty;
2509 best_break = idx;
2510 if penalty == 0 {
2511 break;
2512 }
2513 }
2514 }
2515 best_break
2516}
2517
2518fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
2519 while idx < boundaries.len() - 1 {
2520 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
2521 if !ch.chars().all(char::is_whitespace) {
2522 break;
2523 }
2524 idx += 1;
2525 }
2526 idx
2527}
2528
2529fn apply_overflow<M: TextMeasurer + ?Sized>(
2534 measurer: &M,
2535 node_id: Option<NodeId>,
2536 (text, style): (&crate::text::AnnotatedString, &TextStyle),
2537 options: TextLayoutOptions,
2538 max_width: Option<f32>,
2539 (visible_lines, hold): (&mut DisplayLines, &mut Option<WrapHold>),
2540) -> bool {
2541 if options.overflow == TextOverflow::Visible {
2542 return false;
2543 }
2544 let ellipsis = EllipsisPlacement::for_options(options);
2545 let mut did_overflow = false;
2546 if visible_lines.len() > options.max_lines {
2547 did_overflow = true;
2548 visible_lines.truncate(options.max_lines);
2549 if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
2550 last_line.extend_to_paragraph_end(text);
2551 let cut = last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
2552 if let Some(hold) = hold {
2553 hold.narrow(cut.fits, cut.pulls_up);
2554 }
2555 }
2556 }
2557
2558 let Some(width_limit) = max_width else {
2559 return did_overflow;
2560 };
2561 let visible_len = visible_lines.len();
2562 for (line_index, line) in visible_lines.iter_mut().enumerate() {
2563 if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2564 continue;
2565 }
2566 did_overflow = true;
2567 *hold = None;
2568 if line_index + 1 == visible_len
2569 && let Some(placement) = ellipsis
2570 {
2571 line.ellipsize(measurer, node_id, text, style, max_width, placement);
2572 }
2573 }
2574 did_overflow
2575}
2576
2577#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2578enum EllipsisPlacement {
2579 End,
2580 Start,
2581 Middle,
2582}
2583
2584impl EllipsisPlacement {
2585 fn split(self, kept_chars: usize) -> (usize, usize) {
2587 match self {
2588 Self::End => (kept_chars, 0),
2589 Self::Start => (0, kept_chars),
2590 Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2591 }
2592 }
2593
2594 fn estimated_kept_chars(
2599 self,
2600 prefix_widths: &TextLinePrefixWidths,
2601 ellipsis_width: f32,
2602 width_limit: f32,
2603 ) -> Option<usize> {
2604 let char_count = prefix_widths.char_count();
2605 let width = |kept_chars: usize| {
2606 let (head_chars, tail_chars) = self.split(kept_chars);
2607 Some(
2608 prefix_widths.width_for_char_range(0, head_chars)?
2609 + ellipsis_width
2610 + prefix_widths.width_for_char_range(char_count - tail_chars, char_count)?,
2611 )
2612 };
2613 let (mut fitting, mut overflowing) = (0usize, char_count + 1);
2614 while fitting + 1 < overflowing {
2615 let kept_chars = fitting + (overflowing - fitting) / 2;
2616 if width(kept_chars)? <= width_limit + WRAP_EPSILON {
2617 fitting = kept_chars;
2618 } else {
2619 overflowing = kept_chars;
2620 }
2621 }
2622 Some(fitting)
2623 }
2624
2625 fn for_options(options: TextLayoutOptions) -> Option<Self> {
2626 let single_line = options.max_lines == 1;
2627 match options.overflow {
2628 TextOverflow::Ellipsis => Some(Self::End),
2629 TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2630 TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2631 TextOverflow::StartEllipsis
2632 | TextOverflow::MiddleEllipsis
2633 | TextOverflow::Clip
2634 | TextOverflow::Visible
2635 | TextOverflow::ScaleDown { .. } => None,
2636 }
2637 }
2638
2639 fn elide(
2640 self,
2641 source: &crate::text::AnnotatedString,
2642 source_range: Range<usize>,
2643 boundaries: &[usize],
2644 kept_chars: usize,
2645 ) -> crate::text::AnnotatedString {
2646 let char_count = boundaries.len() - 1;
2647 let (head_chars, tail_chars) = self.split(kept_chars);
2648 let head_end = source_range.start + boundaries[head_chars];
2649 let tail_start = source_range.start + boundaries[char_count - tail_chars];
2650 crate::text::AnnotatedString::builder()
2651 .append_annotated_subsequence(source, source_range.start..head_end)
2652 .append(ELLIPSIS)
2653 .append_annotated_subsequence(source, tail_start..source_range.end)
2654 .to_annotated_string()
2655 }
2656}
2657
2658fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2662 measurer: &M,
2663 node_id: Option<NodeId>,
2664 source: &crate::text::AnnotatedString,
2665 source_range: Range<usize>,
2666 style: &TextStyle,
2667 max_width: Option<f32>,
2668 placement: EllipsisPlacement,
2669) -> (DisplayLine, WrapHold) {
2670 let width_limit = max_width.unwrap_or(f32::INFINITY);
2671 let fitting_line = |text: DisplayLineText| {
2673 let mut line = DisplayLine {
2674 source_range: source_range.clone(),
2675 text,
2676 measured_width: None,
2677 };
2678 let width = line.measure_width(measurer, node_id, source, style);
2679 if width <= width_limit + WRAP_EPSILON {
2680 Ok(line)
2681 } else {
2682 Err(width)
2683 }
2684 };
2685 let whole = if placement == EllipsisPlacement::End {
2688 f32::INFINITY
2689 } else {
2690 match fitting_line(DisplayLineText::Source) {
2691 Ok(line) => return ElisionSearch::found(line, f32::INFINITY),
2692 Err(width) => width,
2693 }
2694 };
2695
2696 let boundaries = char_boundaries(&source.text[source_range.clone()]);
2697 let elided_line = |kept_chars: usize| {
2698 fitting_line(DisplayLineText::Ellipsized(placement.elide(
2699 source,
2700 source_range.clone(),
2701 &boundaries,
2702 kept_chars,
2703 )))
2704 };
2705 let best = match elided_line(0) {
2706 Ok(line) => line,
2707 Err(ellipsis_width) => {
2708 let empty = DisplayLine {
2709 source_range: source_range.clone(),
2710 text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2711 measured_width: None,
2712 };
2713 return ElisionSearch::found(empty, ellipsis_width);
2714 }
2715 };
2716
2717 let guess = best.measured_width.and_then(|ellipsis_width| {
2721 measurer
2722 .measure_line_prefix_widths(source, source_range.clone(), style)
2723 .filter(|widths| widths.char_count() + 1 == boundaries.len())
2724 .and_then(|widths| placement.estimated_kept_chars(&widths, ellipsis_width, width_limit))
2725 });
2726 let mut search = ElisionSearch {
2727 best,
2728 fitting: 0,
2729 overflowing: boundaries.len(),
2730 overflow_width: whole,
2731 };
2732 if let Some(guess) = guess.filter(|guess| *guess > 0) {
2733 for kept_chars in [guess, guess + 1] {
2734 if kept_chars <= search.fitting || kept_chars >= search.overflowing {
2735 break;
2736 }
2737 search.probe(kept_chars, elided_line(kept_chars));
2738 }
2739 }
2740 while search.fitting + 1 < search.overflowing {
2741 let kept_chars = search.fitting + (search.overflowing - search.fitting) / 2;
2742 search.probe(kept_chars, elided_line(kept_chars));
2743 }
2744 ElisionSearch::found(search.best, search.overflow_width)
2745}
2746
2747struct ElisionSearch {
2750 best: DisplayLine,
2751 fitting: usize,
2752 overflowing: usize,
2753 overflow_width: f32,
2754}
2755
2756impl ElisionSearch {
2757 fn probe(&mut self, kept_chars: usize, line: Result<DisplayLine, f32>) {
2758 match line {
2759 Ok(line) => (self.fitting, self.best) = (kept_chars, line),
2760 Err(width) => (self.overflowing, self.overflow_width) = (kept_chars, width),
2761 }
2762 }
2763
2764 fn found(line: DisplayLine, overflow_width: f32) -> (DisplayLine, WrapHold) {
2767 let cut = WrapHold {
2768 fits: line.measured_width.unwrap_or(f32::NEG_INFINITY),
2769 pulls_up: overflow_width,
2770 };
2771 (line, cut)
2772 }
2773}
2774
2775fn char_boundaries(text: &str) -> Vec<usize> {
2776 let mut out = Vec::with_capacity(text.chars().count() + 1);
2777 out.push(0);
2778 for (idx, _) in text.char_indices() {
2779 if idx != 0 {
2780 out.push(idx);
2781 }
2782 }
2783 out.push(text.len());
2784 out
2785}
2786
2787#[cfg(test)]
2788#[path = "tests/measure_tests.rs"]
2789mod tests;