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