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