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 max_width_range(self) -> cranpose_ui_layout::BoundRange {
1806 match self {
1807 Self::Exact(None) => cranpose_ui_layout::BoundRange::exactly(f32::INFINITY),
1808 Self::Exact(Some(bits)) => {
1809 cranpose_ui_layout::BoundRange::exactly(f32::from_bits(bits))
1810 }
1811 Self::AtLeast(min) => cranpose_ui_layout::BoundRange::from(min.next_up()),
1812 Self::Wrapped(hold) => cranpose_ui_layout::BoundRange {
1813 low: (hold.fits - WRAP_EPSILON).next_up(),
1814 high: (hold.pulls_up - WRAP_EPSILON).next_down().next_down(),
1815 },
1816 }
1817 }
1818
1819 pub(crate) fn hold(self, max_width: Option<f32>) -> bool {
1821 let max_width = normalize_max_width(max_width);
1822 match self {
1823 Self::Exact(bits) => max_width.map(f32::to_bits) == bits,
1824 Self::AtLeast(min) => max_width.is_none_or(|width| width >= min),
1825 Self::Wrapped(hold) => max_width.is_some_and(|width| hold.holds(width)),
1826 }
1827 }
1828}
1829
1830fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1831 match max_width {
1832 Some(width) if width.is_finite() && width > 0.0 => Some(width),
1833 _ => None,
1834 }
1835}
1836
1837fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1838 (base_start + relative.start)..(base_start + relative.end)
1839}
1840
1841fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1842 boundaries
1843 .binary_search(&byte_offset)
1844 .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1845}
1846
1847struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1848 measurer: &'a M,
1849 text: &'a crate::text::AnnotatedString,
1850 style: &'a TextStyle,
1851 line_start: usize,
1852 prefix_widths: Option<TextLinePrefixWidths>,
1853}
1854
1855impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1856 fn new(
1857 measurer: &'a M,
1858 text: &'a crate::text::AnnotatedString,
1859 line_range: &Range<usize>,
1860 style: &'a TextStyle,
1861 boundary_count: usize,
1862 ) -> Self {
1863 let expected_chars = boundary_count.saturating_sub(1);
1864 let prefix_widths = measurer
1865 .measure_line_prefix_widths(text, line_range.clone(), style)
1866 .filter(|widths| widths.char_count() == expected_chars);
1867 Self {
1868 measurer,
1869 text,
1870 style,
1871 line_start: line_range.start,
1872 prefix_widths,
1873 }
1874 }
1875
1876 fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1877 if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1878 return width;
1879 }
1880 let segment_range =
1881 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1882 self.measurer
1883 .measure_subsequence(self.text, segment_range, self.style)
1884 .width
1885 }
1886
1887 fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1888 if let Some(prefix_widths) = &self.prefix_widths
1889 && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1890 {
1891 return Some(width);
1892 }
1893 None
1894 }
1895
1896 fn display_line_for_char_range(
1897 &self,
1898 boundaries: &[usize],
1899 start_idx: usize,
1900 end_idx: usize,
1901 ) -> DisplayLine {
1902 let source_range =
1903 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1904 let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1905 DisplayLine::from_measured_source_range(source_range, measured_width)
1906 }
1907}
1908
1909#[derive(Clone, Copy, Debug)]
1914struct LineLimit {
1915 lines: usize,
1916 elides_last: bool,
1917}
1918
1919impl LineLimit {
1920 const NONE: Self = Self {
1921 lines: usize::MAX,
1922 elides_last: false,
1923 };
1924
1925 fn of(options: TextLayoutOptions) -> Self {
1926 Self {
1927 lines: options.max_lines,
1928 elides_last: EllipsisPlacement::for_options(options).is_some(),
1929 }
1930 }
1931
1932 fn reached(self, lines: usize) -> bool {
1935 lines >= self.lines
1936 }
1937
1938 fn elides_next(self, lines: usize) -> bool {
1941 self.elides_last && lines + 1 == self.lines
1942 }
1943}
1944
1945fn wrap_lines<M: TextMeasurer + ?Sized>(
1946 measurer: &M,
1947 text: &crate::text::AnnotatedString,
1948 line_ranges: LineRanges,
1949 style: &TextStyle,
1950 (max_width, limit): (f32, LineLimit),
1951 modes: (LineBreak, Hyphens),
1952) -> (DisplayLines, Option<WrapHold>) {
1953 let source_lines = line_ranges.len();
1954 let mut lines = DisplayLines::with_capacity(source_lines.min(limit.lines.saturating_add(1)));
1955 let mut hold = Some(WrapHold::ANY);
1956 for line_range in line_ranges {
1957 if limit.reached(lines.len()) {
1958 if lines.len() == limit.lines {
1959 lines.push(DisplayLine::from_source_range(line_range));
1960 }
1961 break;
1962 }
1963 wrap_line_to_width(
1964 measurer,
1965 text,
1966 line_range,
1967 style,
1968 (max_width, limit, &mut hold),
1969 modes,
1970 &mut lines,
1971 );
1972 }
1973 let wrapped = lines.len() != source_lines;
1974 (lines, hold.filter(|_| wrapped))
1975}
1976
1977fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1982 measurer: &M,
1983 text: &crate::text::AnnotatedString,
1984 line_range: Range<usize>,
1985 style: &TextStyle,
1986 (max_width, limit, hold): (f32, LineLimit, &mut Option<WrapHold>),
1987 (line_break, hyphens): (LineBreak, Hyphens),
1988 out: &mut DisplayLines,
1989) {
1990 let line_text = &text.text[line_range.clone()];
1991 if line_text.is_empty() {
1992 out.push(DisplayLine::from_source_range(
1993 line_range.start..line_range.start,
1994 ));
1995 return;
1996 }
1997
1998 if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
1999 && measured_width <= max_width + WRAP_EPSILON
2000 {
2001 WrapHold::fit_whole(hold, measured_width);
2002 out.push(DisplayLine::from_measured_source_range(
2003 line_range,
2004 measured_width,
2005 ));
2006 return;
2007 }
2008
2009 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
2010 && line_text.chars().any(char::is_whitespace)
2011 && wrap_line_with_word_balance(
2012 measurer,
2013 text,
2014 line_range.clone(),
2015 style,
2016 max_width,
2017 line_break,
2018 out,
2019 )
2020 {
2021 *hold = None;
2022 return;
2023 }
2024
2025 wrap_line_greedy(
2026 measurer,
2027 text,
2028 line_range,
2029 style,
2030 (max_width, limit, hold),
2031 (line_break, hyphens),
2032 out,
2033 );
2034}
2035
2036fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
2037 measurer: &M,
2038 text: &crate::text::AnnotatedString,
2039 line_range: Range<usize>,
2040 style: &TextStyle,
2041 (max_width, limit, hold): (f32, LineLimit, &mut Option<WrapHold>),
2042 (line_break, hyphens): (LineBreak, Hyphens),
2043 out: &mut DisplayLines,
2044) {
2045 let line_text = &text.text[line_range.clone()];
2046 let boundaries = char_boundaries(line_text);
2047 let measure_context =
2048 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
2049 if let Some(measured_width) =
2050 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
2051 && measured_width <= max_width + WRAP_EPSILON
2052 {
2053 WrapHold::fit_whole(hold, measured_width);
2054 out.push(DisplayLine::from_measured_source_range(
2055 line_range,
2056 measured_width,
2057 ));
2058 return;
2059 }
2060 let first = out.len();
2061 let mut start_idx = 0usize;
2062
2063 while start_idx < boundaries.len() - 1 {
2064 if limit.reached(out.len()) {
2065 out.push(DisplayLine::from_source_range(
2066 line_range.start + boundaries[start_idx]..line_range.end,
2067 ));
2068 return;
2069 }
2070 let mut low = start_idx + 1;
2071 let mut high = boundaries.len() - 1;
2072 let mut best = start_idx + 1;
2073
2074 while low <= high {
2075 let mid = (low + high) / 2;
2076 let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
2077 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
2078 best = mid;
2079 low = mid + 1;
2080 } else {
2081 if mid == 0 {
2082 break;
2083 }
2084 high = mid - 1;
2085 }
2086 }
2087
2088 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
2089 let mut effective_wrap_idx = wrap_idx;
2090 let can_hyphenate = hyphens == Hyphens::Auto
2091 && wrap_idx == best
2092 && best < boundaries.len() - 1
2093 && is_break_inside_word(line_text, &boundaries, wrap_idx);
2094 narrow_to_break(
2095 hold,
2096 &measure_context,
2097 (line_text, &boundaries),
2098 (start_idx, best),
2099 (can_hyphenate, limit.elides_next(out.len())),
2100 );
2101 if can_hyphenate {
2102 effective_wrap_idx = resolve_auto_hyphen_break(
2103 measurer,
2104 line_text,
2105 style,
2106 &boundaries,
2107 start_idx,
2108 wrap_idx,
2109 );
2110 }
2111
2112 let broke_at_word_boundary = effective_wrap_idx > start_idx
2113 && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
2114 .chars()
2115 .all(char::is_whitespace);
2116 let segment_start = boundaries[start_idx];
2117 let mut segment_end = boundaries[effective_wrap_idx];
2118 if wrap_idx != best || broke_at_word_boundary {
2119 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
2120 }
2121 let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
2122 out.push(measure_context.display_line_for_char_range(
2123 &boundaries,
2124 start_idx,
2125 segment_end_idx,
2126 ));
2127
2128 start_idx = if wrap_idx != best || broke_at_word_boundary {
2129 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
2130 } else {
2131 effective_wrap_idx
2132 };
2133 }
2134
2135 if out.len() == first {
2136 out.push(DisplayLine::from_source_range(
2137 line_range.start..line_range.start,
2138 ));
2139 }
2140}
2141
2142fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
2145 measurer: &M,
2146 text: &crate::text::AnnotatedString,
2147 line_range: Range<usize>,
2148 style: &TextStyle,
2149 max_width: f32,
2150 line_break: LineBreak,
2151 out: &mut DisplayLines,
2152) -> bool {
2153 let line_text = &text.text[line_range.clone()];
2154 let boundaries = char_boundaries(line_text);
2155 let measure_context =
2156 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
2157 if let Some(measured_width) =
2158 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
2159 && measured_width <= max_width + WRAP_EPSILON
2160 {
2161 out.push(DisplayLine::from_measured_source_range(
2162 line_range,
2163 measured_width,
2164 ));
2165 return true;
2166 }
2167 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
2168 if breakpoints.len() <= 2 {
2169 return false;
2170 }
2171
2172 let node_count = breakpoints.len();
2173 let mut best_cost = vec![f32::INFINITY; node_count];
2174 let mut next_index = vec![None; node_count];
2175 best_cost[node_count - 1] = 0.0;
2176
2177 for start in (0..node_count - 1).rev() {
2178 for end in start + 1..node_count {
2179 let start_byte = boundaries[breakpoints[start]];
2180 let end_byte = boundaries[breakpoints[end]];
2181 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2182 if trimmed_end <= start_byte {
2183 continue;
2184 }
2185 let segment_start_idx = breakpoints[start];
2186 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2187 let segment_width =
2188 measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
2189 if segment_width > max_width + WRAP_EPSILON {
2190 continue;
2191 }
2192 if !best_cost[end].is_finite() {
2193 continue;
2194 }
2195 let slack = (max_width - segment_width).max(0.0);
2196 let is_last = end == node_count - 1;
2197 let segment_cost = match line_break {
2198 LineBreak::Heading => slack * slack,
2199 LineBreak::Paragraph => {
2200 if is_last {
2201 slack * slack * 0.16
2202 } else {
2203 slack * slack
2204 }
2205 }
2206 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
2207 };
2208 let candidate = segment_cost + best_cost[end];
2209 if candidate < best_cost[start] {
2210 best_cost[start] = candidate;
2211 next_index[start] = Some(end);
2212 }
2213 }
2214 }
2215
2216 let first = out.len();
2217 let mut current = 0usize;
2218 while current < node_count - 1 {
2219 let Some(next) = next_index[current] else {
2220 out.truncate(first);
2221 return false;
2222 };
2223 let start_byte = boundaries[breakpoints[current]];
2224 let end_byte = boundaries[breakpoints[next]];
2225 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2226 if trimmed_end <= start_byte {
2227 out.truncate(first);
2228 return false;
2229 }
2230 let segment_start_idx = breakpoints[current];
2231 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2232 out.push(measure_context.display_line_for_char_range(
2233 &boundaries,
2234 segment_start_idx,
2235 segment_end_idx,
2236 ));
2237 current = next;
2238 }
2239 out.len() > first
2240}
2241
2242fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
2243 let mut points = vec![0usize];
2244 for idx in 1..boundaries.len() - 1 {
2245 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
2246 let current = &line[boundaries[idx]..boundaries[idx + 1]];
2247 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
2248 points.push(idx);
2249 }
2250 }
2251 let end = boundaries.len() - 1;
2252 if points.last().copied() != Some(end) {
2253 points.push(end);
2254 }
2255 points
2256}
2257
2258fn choose_wrap_break(
2259 line: &str,
2260 boundaries: &[usize],
2261 start_idx: usize,
2262 best: usize,
2263 _line_break: LineBreak,
2264) -> usize {
2265 if best >= boundaries.len() - 1 {
2266 return best;
2267 }
2268
2269 if best <= start_idx + 1 {
2270 return best;
2271 }
2272
2273 for idx in (start_idx + 1..=best).rev() {
2274 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
2275 if prev.chars().all(char::is_whitespace) {
2276 return idx;
2277 }
2278 }
2279 best
2280}
2281
2282fn narrow_to_break<M: TextMeasurer + ?Sized>(
2289 hold: &mut Option<WrapHold>,
2290 measure_context: &LineMeasureContext<'_, M>,
2291 (line, boundaries): (&str, &[usize]),
2292 (start_idx, best): (usize, usize),
2293 (hyphenated, elided): (bool, bool),
2294) {
2295 let end = boundaries.len() - 1;
2296 if elided && best < end {
2297 if let Some(hold) = hold {
2298 let rest = measure_context.measure_char_range(boundaries, start_idx, end);
2299 hold.narrow(f32::NEG_INFINITY, rest);
2300 }
2301 return;
2302 }
2303 if hyphenated {
2304 *hold = None;
2305 }
2306 let Some(hold) = hold else {
2307 return;
2308 };
2309 let (fits, pulls_up) = wrap_break_widths(line, boundaries, start_idx, best);
2310 let width = |idx| measure_context.measure_char_range(boundaries, start_idx, idx);
2311 hold.narrow(
2312 fits.map_or(f32::NEG_INFINITY, width),
2313 pulls_up.map_or(f32::INFINITY, width),
2314 );
2315}
2316
2317fn wrap_break_widths(
2323 line: &str,
2324 boundaries: &[usize],
2325 start_idx: usize,
2326 best: usize,
2327) -> (Option<usize>, Option<usize>) {
2328 let end = boundaries.len() - 1;
2329 if best <= start_idx + 1 {
2331 return (None, (best < end).then_some(best + 1));
2332 }
2333 if best >= end {
2334 return (Some(end), None);
2335 }
2336 let after_space = |idx: usize| {
2337 line[boundaries[idx - 1]..boundaries[idx]]
2338 .chars()
2339 .all(char::is_whitespace)
2340 };
2341 match (start_idx + 1..=best).rev().find(|&idx| after_space(idx)) {
2342 Some(wrap_idx) => {
2343 let next = (best + 1..end).find(|&idx| after_space(idx)).unwrap_or(end);
2344 (Some(wrap_idx), Some(next))
2345 }
2346 None => (Some(best), Some(best + 1)),
2347 }
2348}
2349
2350fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
2351 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
2352 return false;
2353 }
2354 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
2355 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
2356 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
2357}
2358
2359fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
2360 measurer: &M,
2361 line: &str,
2362 style: &TextStyle,
2363 boundaries: &[usize],
2364 start_idx: usize,
2365 break_idx: usize,
2366) -> usize {
2367 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
2368 && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
2369 {
2370 return candidate;
2371 }
2372 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
2373}
2374
2375fn is_valid_auto_hyphen_break(
2376 line: &str,
2377 boundaries: &[usize],
2378 start_idx: usize,
2379 break_idx: usize,
2380 candidate_idx: usize,
2381) -> bool {
2382 let end_idx = boundaries.len().saturating_sub(1);
2383 candidate_idx > start_idx
2384 && candidate_idx < end_idx
2385 && candidate_idx <= break_idx
2386 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
2387 && is_break_inside_word(line, boundaries, candidate_idx)
2388}
2389
2390fn choose_auto_hyphen_break_fallback(
2391 boundaries: &[usize],
2392 start_idx: usize,
2393 break_idx: usize,
2394) -> usize {
2395 let end_idx = boundaries.len().saturating_sub(1);
2396 if break_idx >= end_idx {
2397 return break_idx;
2398 }
2399 let trailing_len = end_idx.saturating_sub(break_idx);
2400 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
2401 return break_idx;
2402 }
2403
2404 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
2405 let max_break = break_idx.saturating_sub(1);
2406 if min_break > max_break {
2407 return break_idx;
2408 }
2409
2410 let mut best_break = break_idx;
2411 let mut best_penalty = usize::MAX;
2412 for idx in min_break..=max_break {
2413 let candidate_trailing_len = end_idx.saturating_sub(idx);
2414 let candidate_prefix_len = idx.saturating_sub(start_idx);
2415 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
2416 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
2417 {
2418 continue;
2419 }
2420
2421 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
2422 if penalty < best_penalty {
2423 best_penalty = penalty;
2424 best_break = idx;
2425 if penalty == 0 {
2426 break;
2427 }
2428 }
2429 }
2430 best_break
2431}
2432
2433fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
2434 while idx < boundaries.len() - 1 {
2435 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
2436 if !ch.chars().all(char::is_whitespace) {
2437 break;
2438 }
2439 idx += 1;
2440 }
2441 idx
2442}
2443
2444fn apply_overflow<M: TextMeasurer + ?Sized>(
2449 measurer: &M,
2450 node_id: Option<NodeId>,
2451 (text, style): (&crate::text::AnnotatedString, &TextStyle),
2452 options: TextLayoutOptions,
2453 max_width: Option<f32>,
2454 (visible_lines, hold): (&mut DisplayLines, &mut Option<WrapHold>),
2455) -> bool {
2456 if options.overflow == TextOverflow::Visible {
2457 return false;
2458 }
2459 let ellipsis = EllipsisPlacement::for_options(options);
2460 let mut did_overflow = false;
2461 if visible_lines.len() > options.max_lines {
2462 did_overflow = true;
2463 visible_lines.truncate(options.max_lines);
2464 if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
2465 last_line.extend_to_paragraph_end(text);
2466 let cut = last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
2467 if let Some(hold) = hold {
2468 hold.narrow(cut.fits, cut.pulls_up);
2469 }
2470 }
2471 }
2472
2473 let Some(width_limit) = max_width else {
2474 return did_overflow;
2475 };
2476 let visible_len = visible_lines.len();
2477 for (line_index, line) in visible_lines.iter_mut().enumerate() {
2478 if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2479 continue;
2480 }
2481 did_overflow = true;
2482 *hold = None;
2483 if line_index + 1 == visible_len
2484 && let Some(placement) = ellipsis
2485 {
2486 line.ellipsize(measurer, node_id, text, style, max_width, placement);
2487 }
2488 }
2489 did_overflow
2490}
2491
2492#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2493enum EllipsisPlacement {
2494 End,
2495 Start,
2496 Middle,
2497}
2498
2499impl EllipsisPlacement {
2500 fn split(self, kept_chars: usize) -> (usize, usize) {
2502 match self {
2503 Self::End => (kept_chars, 0),
2504 Self::Start => (0, kept_chars),
2505 Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2506 }
2507 }
2508
2509 fn estimated_kept_chars(
2514 self,
2515 prefix_widths: &TextLinePrefixWidths,
2516 ellipsis_width: f32,
2517 width_limit: f32,
2518 ) -> Option<usize> {
2519 let char_count = prefix_widths.char_count();
2520 let width = |kept_chars: usize| {
2521 let (head_chars, tail_chars) = self.split(kept_chars);
2522 Some(
2523 prefix_widths.width_for_char_range(0, head_chars)?
2524 + ellipsis_width
2525 + prefix_widths.width_for_char_range(char_count - tail_chars, char_count)?,
2526 )
2527 };
2528 let (mut fitting, mut overflowing) = (0usize, char_count + 1);
2529 while fitting + 1 < overflowing {
2530 let kept_chars = fitting + (overflowing - fitting) / 2;
2531 if width(kept_chars)? <= width_limit + WRAP_EPSILON {
2532 fitting = kept_chars;
2533 } else {
2534 overflowing = kept_chars;
2535 }
2536 }
2537 Some(fitting)
2538 }
2539
2540 fn for_options(options: TextLayoutOptions) -> Option<Self> {
2541 let single_line = options.max_lines == 1;
2542 match options.overflow {
2543 TextOverflow::Ellipsis => Some(Self::End),
2544 TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2545 TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2546 TextOverflow::StartEllipsis
2547 | TextOverflow::MiddleEllipsis
2548 | TextOverflow::Clip
2549 | TextOverflow::Visible
2550 | TextOverflow::ScaleDown { .. } => None,
2551 }
2552 }
2553
2554 fn elide(
2555 self,
2556 source: &crate::text::AnnotatedString,
2557 source_range: Range<usize>,
2558 boundaries: &[usize],
2559 kept_chars: usize,
2560 ) -> crate::text::AnnotatedString {
2561 let char_count = boundaries.len() - 1;
2562 let (head_chars, tail_chars) = self.split(kept_chars);
2563 let head_end = source_range.start + boundaries[head_chars];
2564 let tail_start = source_range.start + boundaries[char_count - tail_chars];
2565 crate::text::AnnotatedString::builder()
2566 .append_annotated_subsequence(source, source_range.start..head_end)
2567 .append(ELLIPSIS)
2568 .append_annotated_subsequence(source, tail_start..source_range.end)
2569 .to_annotated_string()
2570 }
2571}
2572
2573fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2577 measurer: &M,
2578 node_id: Option<NodeId>,
2579 source: &crate::text::AnnotatedString,
2580 source_range: Range<usize>,
2581 style: &TextStyle,
2582 max_width: Option<f32>,
2583 placement: EllipsisPlacement,
2584) -> (DisplayLine, WrapHold) {
2585 let width_limit = max_width.unwrap_or(f32::INFINITY);
2586 let fitting_line = |text: DisplayLineText| {
2588 let mut line = DisplayLine {
2589 source_range: source_range.clone(),
2590 text,
2591 measured_width: None,
2592 };
2593 let width = line.measure_width(measurer, node_id, source, style);
2594 if width <= width_limit + WRAP_EPSILON {
2595 Ok(line)
2596 } else {
2597 Err(width)
2598 }
2599 };
2600 let whole = if placement == EllipsisPlacement::End {
2603 f32::INFINITY
2604 } else {
2605 match fitting_line(DisplayLineText::Source) {
2606 Ok(line) => return ElisionSearch::found(line, f32::INFINITY),
2607 Err(width) => width,
2608 }
2609 };
2610
2611 let boundaries = char_boundaries(&source.text[source_range.clone()]);
2612 let elided_line = |kept_chars: usize| {
2613 fitting_line(DisplayLineText::Ellipsized(placement.elide(
2614 source,
2615 source_range.clone(),
2616 &boundaries,
2617 kept_chars,
2618 )))
2619 };
2620 let best = match elided_line(0) {
2621 Ok(line) => line,
2622 Err(ellipsis_width) => {
2623 let empty = DisplayLine {
2624 source_range: source_range.clone(),
2625 text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2626 measured_width: None,
2627 };
2628 return ElisionSearch::found(empty, ellipsis_width);
2629 }
2630 };
2631
2632 let guess = best.measured_width.and_then(|ellipsis_width| {
2636 measurer
2637 .measure_line_prefix_widths(source, source_range.clone(), style)
2638 .filter(|widths| widths.char_count() + 1 == boundaries.len())
2639 .and_then(|widths| placement.estimated_kept_chars(&widths, ellipsis_width, width_limit))
2640 });
2641 let mut search = ElisionSearch {
2642 best,
2643 fitting: 0,
2644 overflowing: boundaries.len(),
2645 overflow_width: whole,
2646 };
2647 if let Some(guess) = guess.filter(|guess| *guess > 0) {
2648 for kept_chars in [guess, guess + 1] {
2649 if kept_chars <= search.fitting || kept_chars >= search.overflowing {
2650 break;
2651 }
2652 search.probe(kept_chars, elided_line(kept_chars));
2653 }
2654 }
2655 while search.fitting + 1 < search.overflowing {
2656 let kept_chars = search.fitting + (search.overflowing - search.fitting) / 2;
2657 search.probe(kept_chars, elided_line(kept_chars));
2658 }
2659 ElisionSearch::found(search.best, search.overflow_width)
2660}
2661
2662struct ElisionSearch {
2665 best: DisplayLine,
2666 fitting: usize,
2667 overflowing: usize,
2668 overflow_width: f32,
2669}
2670
2671impl ElisionSearch {
2672 fn probe(&mut self, kept_chars: usize, line: Result<DisplayLine, f32>) {
2673 match line {
2674 Ok(line) => (self.fitting, self.best) = (kept_chars, line),
2675 Err(width) => (self.overflowing, self.overflow_width) = (kept_chars, width),
2676 }
2677 }
2678
2679 fn found(line: DisplayLine, overflow_width: f32) -> (DisplayLine, WrapHold) {
2682 let cut = WrapHold {
2683 fits: line.measured_width.unwrap_or(f32::NEG_INFINITY),
2684 pulls_up: overflow_width,
2685 };
2686 (line, cut)
2687 }
2688}
2689
2690fn char_boundaries(text: &str) -> Vec<usize> {
2691 let mut out = Vec::with_capacity(text.chars().count() + 1);
2692 out.push(0);
2693 for (idx, _) in text.char_indices() {
2694 if idx != 0 {
2695 out.push(idx);
2696 }
2697 }
2698 out.push(text.len());
2699 out
2700}
2701
2702#[cfg(test)]
2703#[path = "tests/measure_tests.rs"]
2704mod tests;