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