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 lines = Vec::with_capacity(line_ranges.len());
962 for line_range in line_ranges {
963 wrap_line_to_width(
964 measurer,
965 text,
966 line_range,
967 style,
968 width_limit,
969 line_break_mode,
970 hyphens_mode,
971 &mut lines,
972 );
973 }
974 lines.into_iter().map(|line| line.source_range).collect()
975}
976
977fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
978 measurer: &M,
979 text: &crate::text::AnnotatedString,
980 style: &TextStyle,
981 options: TextLayoutOptions,
982 max_width: Option<f32>,
983) -> PreparedTextLayout {
984 prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
985}
986
987pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
988 measurer: &M,
989 node_id: Option<NodeId>,
990 text: &crate::text::AnnotatedString,
991 style: &TextStyle,
992 options: TextLayoutOptions,
993 max_width: Option<f32>,
994) -> PreparedTextLayout {
995 let telemetry = text_layout_telemetry_enabled();
996 let total_start = telemetry.then(Instant::now);
997 let opts = options.normalized();
998 let max_width = normalize_max_width(max_width);
999 if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
1000 return prepare_scale_down_text_layout(
1001 measurer,
1002 node_id,
1003 text,
1004 style,
1005 opts,
1006 max_width,
1007 min_font_size_sp,
1008 );
1009 }
1010
1011 let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
1012 .then_some(max_width)
1013 .flatten();
1014 let line_break_mode = style
1015 .paragraph_style
1016 .line_break
1017 .take_or_else(|| LineBreak::Simple);
1018 let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
1019
1020 let wrap_start = telemetry.then(Instant::now);
1021 let line_ranges = split_line_ranges(text.text.as_str());
1022 let source_line_count = line_ranges.len();
1023 let mut visible_lines: Vec<DisplayLine>;
1024 if let Some(width_limit) = wrap_width {
1025 visible_lines = Vec::with_capacity(line_ranges.len());
1026 for line_range in line_ranges {
1027 wrap_line_to_width(
1028 measurer,
1029 text,
1030 line_range,
1031 style,
1032 width_limit,
1033 line_break_mode,
1034 hyphens_mode,
1035 &mut visible_lines,
1036 );
1037 }
1038 } else {
1039 visible_lines = line_ranges
1040 .into_iter()
1041 .map(DisplayLine::from_source_range)
1042 .collect();
1043 }
1044 let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1045
1046 let overflow_start = telemetry.then(Instant::now);
1047 let did_overflow = apply_overflow(
1048 measurer,
1049 node_id,
1050 text,
1051 style,
1052 opts,
1053 max_width,
1054 &mut visible_lines,
1055 );
1056 let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1057
1058 let build_start = telemetry.then(Instant::now);
1059 let display_annotated = build_display_annotated(text, &visible_lines);
1060 debug_assert_eq!(
1061 display_annotated.text,
1062 join_display_line_text(text, &visible_lines)
1063 );
1064 let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1065
1066 let metrics_start = telemetry.then(Instant::now);
1067 let line_height = measurer.line_height_for_node(node_id, text, style).max(0.0);
1068 let display_line_count = visible_lines.len().max(1);
1069 let layout_line_count = display_line_count.max(opts.min_lines);
1070
1071 let measured_width = if visible_lines.is_empty() {
1072 0.0
1073 } else {
1074 visible_lines
1075 .iter()
1076 .map(|line| line.measure_width(measurer, node_id, text, style))
1077 .fold(0.0_f32, f32::max)
1078 };
1079 let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1080 let width = if opts.overflow == TextOverflow::Visible {
1081 measured_width
1082 } else if let Some(width_limit) = max_width {
1083 measured_width.min(width_limit)
1084 } else {
1085 measured_width
1086 };
1087
1088 let edges = measurer
1089 .line_box(style)
1090 .unwrap_or_else(|| crate::text::LineBox::untrimmed(line_height, 0.0));
1091 let prepared = PreparedTextLayout {
1092 text: Rc::new(display_annotated),
1093 visual_style: style.clone(),
1094 metrics: TextMetrics {
1095 width,
1096 height: (layout_line_count as f32 * line_height - edges.trim_top - edges.trim_bottom)
1097 .max(0.0),
1098 line_height,
1099 line_count: layout_line_count,
1100 },
1101 did_overflow,
1102 render_text: Default::default(),
1103 };
1104
1105 if let Some(start) = total_start {
1106 eprintln!(
1107 "[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}",
1108 text.text.len(),
1109 text.span_styles.len(),
1110 source_line_count,
1111 display_line_count,
1112 wrap_width.is_some(),
1113 max_width,
1114 wrap_ms.unwrap_or(0.0),
1115 overflow_ms.unwrap_or(0.0),
1116 build_ms.unwrap_or(0.0),
1117 metrics_ms.unwrap_or(0.0),
1118 start.elapsed().as_secs_f64() * 1000.0,
1119 );
1120 }
1121
1122 prepared
1123}
1124
1125fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1126 measurer: &M,
1127 node_id: Option<NodeId>,
1128 text: &crate::text::AnnotatedString,
1129 style: &TextStyle,
1130 options: TextLayoutOptions,
1131 max_width: Option<f32>,
1132 min_font_size_sp: f32,
1133) -> PreparedTextLayout {
1134 let clipped_options = TextLayoutOptions {
1135 overflow: TextOverflow::Clip,
1136 ..options
1137 }
1138 .normalized();
1139
1140 let full_size = prepare_scaled_text_layout(
1141 measurer,
1142 node_id,
1143 text,
1144 style,
1145 clipped_options,
1146 max_width,
1147 FontScaleCurve::linear(1.0),
1148 );
1149 let Some(width_limit) = max_width else {
1150 return full_size;
1151 };
1152 if !full_size.did_overflow {
1153 return full_size;
1154 }
1155
1156 let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1157 if !base_font_size.is_finite() || base_font_size <= 0.0 {
1158 return full_size;
1159 }
1160 let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1161 if min_scale >= 1.0 {
1162 return full_size;
1163 }
1164
1165 let min_size = prepare_scaled_text_layout(
1166 measurer,
1167 node_id,
1168 text,
1169 style,
1170 clipped_options,
1171 Some(width_limit),
1172 FontScaleCurve::linear(min_scale),
1173 );
1174 if min_size.did_overflow {
1175 return min_size;
1176 }
1177
1178 let mut low = min_scale;
1179 let mut high = 1.0;
1180 let mut best = min_size;
1181 for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1182 let mid = (low + high) * 0.5;
1183 let candidate = prepare_scaled_text_layout(
1184 measurer,
1185 node_id,
1186 text,
1187 style,
1188 clipped_options,
1189 Some(width_limit),
1190 FontScaleCurve::linear(mid),
1191 );
1192 if candidate.did_overflow {
1193 high = mid;
1194 } else {
1195 low = mid;
1196 best = candidate;
1197 }
1198 }
1199
1200 best
1201}
1202
1203fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1204 measurer: &M,
1205 node_id: Option<NodeId>,
1206 text: &crate::text::AnnotatedString,
1207 style: &TextStyle,
1208 options: TextLayoutOptions,
1209 max_width: Option<f32>,
1210 shrink: FontScaleCurve,
1211) -> PreparedTextLayout {
1212 let visual_style = scale_text_style_font_sizes(style, shrink);
1213 let visual_text = scale_annotated_font_sizes(text, shrink);
1214 prepare_text_layout_with_measurer_for_node(
1215 measurer,
1216 node_id,
1217 visual_text.as_ref(),
1218 &visual_style,
1219 options,
1220 max_width,
1221 )
1222}
1223
1224fn scale_annotated_font_sizes(
1225 text: &crate::text::AnnotatedString,
1226 curve: FontScaleCurve,
1227) -> Cow<'_, crate::text::AnnotatedString> {
1228 if curve.is_identity() || !annotated_text_needs_scaling(text) {
1229 return Cow::Borrowed(text);
1230 }
1231
1232 let mut scaled = text.clone();
1233 for span in &mut scaled.span_styles {
1234 span.item = scale_span_style_font_sizes(&span.item, curve, None);
1235 }
1236 Cow::Owned(scaled)
1237}
1238
1239fn scale_text_style_font_sizes(style: &TextStyle, curve: FontScaleCurve) -> TextStyle {
1240 if curve.is_identity() {
1241 return style.clone();
1242 }
1243
1244 let mut scaled = style.clone();
1245 scaled.span_style =
1246 scale_span_style_font_sizes(&style.span_style, curve, Some(DEFAULT_FONT_SIZE_SP));
1247 scaled.paragraph_style.line_height =
1248 scale_text_unit_sp(scaled.paragraph_style.line_height, curve);
1249 if let Some(mut indent) = scaled.paragraph_style.text_indent {
1250 indent.first_line = scale_text_unit_sp(indent.first_line, curve);
1251 indent.rest_line = scale_text_unit_sp(indent.rest_line, curve);
1252 scaled.paragraph_style.text_indent = Some(indent);
1253 }
1254 scaled
1255}
1256
1257fn with_system_font_scale<R>(
1258 text: &crate::text::AnnotatedString,
1259 style: &TextStyle,
1260 block: impl FnOnce(&crate::text::AnnotatedString, &TextStyle) -> R,
1261) -> R {
1262 let curve = crate::current_font_scale_curve();
1263 let visual_style = scale_text_style_font_sizes(style, curve);
1264 let visual_text = scale_annotated_font_sizes(text, curve);
1265 block(visual_text.as_ref(), &visual_style)
1266}
1267
1268fn scale_span_style_font_sizes(
1269 style: &crate::text::SpanStyle,
1270 curve: FontScaleCurve,
1271 default_font_size_sp: Option<f32>,
1272) -> crate::text::SpanStyle {
1273 let factor = curve.scale();
1274 let mut scaled = style.clone();
1275 scaled.font_size = match (style.font_size, default_font_size_sp) {
1276 (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1277 crate::text::TextUnit::Sp(curve.sp_to_dp(default_size))
1278 }
1279 (unit, Some(_)) => scale_text_unit_sp_and_em(unit, curve),
1280 (unit, None) => scale_text_unit_sp(unit, curve),
1281 };
1282 scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, curve);
1283 if let Some(mut shadow) = scaled.shadow {
1284 shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1285 shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1286 shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1287 scaled.shadow = Some(shadow);
1288 }
1289 if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1290 scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1291 width: width * factor,
1292 });
1293 }
1294 scaled
1295}
1296
1297fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1298 text.span_styles
1299 .iter()
1300 .any(|span| span_style_needs_scaling(&span.item))
1301}
1302
1303fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1304 matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1305 || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1306 || matches!(
1307 style.draw_style,
1308 Some(crate::text::TextDrawStyle::Stroke { .. })
1309 )
1310 || style.shadow.is_some()
1311}
1312
1313fn scale_text_unit_sp(unit: crate::text::TextUnit, curve: FontScaleCurve) -> crate::text::TextUnit {
1314 match unit {
1315 crate::text::TextUnit::Sp(value) if value.is_finite() => {
1316 crate::text::TextUnit::Sp(curve.sp_to_dp(value))
1317 }
1318 other => other,
1319 }
1320}
1321
1322fn scale_text_unit_sp_and_em(
1323 unit: crate::text::TextUnit,
1324 curve: FontScaleCurve,
1325) -> crate::text::TextUnit {
1326 match unit {
1327 crate::text::TextUnit::Sp(_) => scale_text_unit_sp(unit, curve),
1328 crate::text::TextUnit::Em(value) if value.is_finite() => {
1329 crate::text::TextUnit::Em(value * curve.scale())
1330 }
1331 other => other,
1332 }
1333}
1334
1335fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1336 if value.is_finite() {
1337 value * factor
1338 } else {
1339 value
1340 }
1341}
1342
1343#[derive(Clone, Debug)]
1344enum DisplayLineText {
1345 Source,
1346 Ellipsized(crate::text::AnnotatedString),
1347}
1348
1349#[derive(Clone, Debug)]
1350struct DisplayLine {
1351 source_range: Range<usize>,
1352 text: DisplayLineText,
1353 measured_width: Option<f32>,
1354}
1355
1356impl DisplayLine {
1357 fn from_source_range(source_range: Range<usize>) -> Self {
1358 Self {
1359 source_range,
1360 text: DisplayLineText::Source,
1361 measured_width: None,
1362 }
1363 }
1364
1365 fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1366 Self {
1367 source_range,
1368 text: DisplayLineText::Source,
1369 measured_width: measured_width
1370 .is_finite()
1371 .then_some(measured_width.max(0.0)),
1372 }
1373 }
1374
1375 fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1376 match &self.text {
1377 DisplayLineText::Source => &source.text[self.source_range.clone()],
1378 DisplayLineText::Ellipsized(annotated) => annotated.text.as_str(),
1379 }
1380 }
1381
1382 fn measure_width<M: TextMeasurer + ?Sized>(
1383 &self,
1384 measurer: &M,
1385 node_id: Option<NodeId>,
1386 source: &crate::text::AnnotatedString,
1387 style: &TextStyle,
1388 ) -> f32 {
1389 self.measured_width.unwrap_or_else(|| match &self.text {
1390 DisplayLineText::Source => {
1391 measurer
1392 .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1393 .width
1394 }
1395 DisplayLineText::Ellipsized(annotated) => {
1396 measurer.measure_for_node(node_id, annotated, style).width
1397 }
1398 })
1399 }
1400
1401 fn extend_to_paragraph_end(&mut self, source: &crate::text::AnnotatedString) {
1402 let start = self.source_range.start;
1403 let end = source.text[start..]
1404 .find('\n')
1405 .map_or(source.text.len(), |offset| start + offset);
1406 self.source_range = start..end;
1407 self.text = DisplayLineText::Source;
1408 self.measured_width = None;
1409 }
1410
1411 fn ellipsize<M: TextMeasurer + ?Sized>(
1412 &mut self,
1413 measurer: &M,
1414 node_id: Option<NodeId>,
1415 source: &crate::text::AnnotatedString,
1416 style: &TextStyle,
1417 max_width: Option<f32>,
1418 placement: EllipsisPlacement,
1419 ) {
1420 *self = fit_ellipsis(
1421 measurer,
1422 node_id,
1423 source,
1424 self.source_range.clone(),
1425 style,
1426 max_width,
1427 placement,
1428 );
1429 }
1430}
1431
1432fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1433 if text.is_empty() {
1434 return single_line_range(0..0);
1435 }
1436
1437 let mut ranges = Vec::new();
1438 let mut start = 0usize;
1439 for (idx, ch) in text.char_indices() {
1440 if ch == '\n' {
1441 ranges.push(start..idx);
1442 start = idx + ch.len_utf8();
1443 }
1444 }
1445 ranges.push(start..text.len());
1446 ranges
1447}
1448
1449fn build_display_annotated(
1450 source: &crate::text::AnnotatedString,
1451 lines: &[DisplayLine],
1452) -> crate::text::AnnotatedString {
1453 if lines.is_empty() {
1454 return crate::text::AnnotatedString::from("");
1455 }
1456
1457 let mut builder = crate::text::AnnotatedString::builder();
1458 for (idx, line) in lines.iter().enumerate() {
1459 builder = match &line.text {
1460 DisplayLineText::Source => {
1461 builder.append_annotated_subsequence(source, line.source_range.clone())
1462 }
1463 DisplayLineText::Ellipsized(annotated) => builder.append_annotated(annotated),
1464 };
1465 if idx + 1 < lines.len() {
1466 builder = builder.append("\n");
1467 }
1468 }
1469 builder.to_annotated_string()
1470}
1471
1472fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1473 let mut text = String::new();
1474 for (idx, line) in lines.iter().enumerate() {
1475 text.push_str(line.display_text(source));
1476 if idx + 1 < lines.len() {
1477 text.push('\n');
1478 }
1479 }
1480 text
1481}
1482
1483fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1484 while end > start {
1485 let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1486 break;
1487 };
1488 if ch.is_whitespace() {
1489 end = start + idx;
1490 } else {
1491 break;
1492 }
1493 }
1494 end
1495}
1496
1497#[derive(Clone, Copy, Debug, PartialEq)]
1500pub(crate) enum PreparedWidths {
1501 Exact(Option<u32>),
1503 AtLeast(f32),
1506}
1507
1508impl PreparedWidths {
1509 pub(crate) fn of(
1511 text: &crate::text::AnnotatedString,
1512 options: TextLayoutOptions,
1513 max_width: Option<f32>,
1514 prepared: &PreparedTextLayout,
1515 ) -> Self {
1516 let max_width = normalize_max_width(max_width);
1517 let exact = Self::Exact(max_width.map(f32::to_bits));
1518 let wrapped = prepared.text.text.matches('\n').count() != text.text.matches('\n').count();
1519 let trailing_space = text
1522 .text
1523 .split('\n')
1524 .any(|line| line.ends_with(char::is_whitespace));
1525 if options
1526 .normalized()
1527 .overflow
1528 .scale_down_min_font_size_sp()
1529 .is_some()
1530 || prepared.did_overflow
1531 || wrapped
1532 || trailing_space
1533 {
1534 return exact;
1535 }
1536 match max_width {
1537 Some(width) if prepared.metrics.width >= width => exact,
1538 _ => Self::AtLeast(prepared.metrics.width),
1539 }
1540 }
1541
1542 pub(crate) fn hold(self, max_width: Option<f32>) -> bool {
1544 let max_width = normalize_max_width(max_width);
1545 match self {
1546 Self::Exact(bits) => max_width.map(f32::to_bits) == bits,
1547 Self::AtLeast(min) => max_width.is_none_or(|width| width >= min),
1548 }
1549 }
1550}
1551
1552fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1553 match max_width {
1554 Some(width) if width.is_finite() && width > 0.0 => Some(width),
1555 _ => None,
1556 }
1557}
1558
1559fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1560 (base_start + relative.start)..(base_start + relative.end)
1561}
1562
1563fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1564 boundaries
1565 .binary_search(&byte_offset)
1566 .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1567}
1568
1569fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1570 std::iter::once(range).collect()
1571}
1572
1573struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1574 measurer: &'a M,
1575 text: &'a crate::text::AnnotatedString,
1576 style: &'a TextStyle,
1577 line_start: usize,
1578 prefix_widths: Option<Rc<TextLinePrefixWidths>>,
1579}
1580
1581impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1582 fn new(
1583 measurer: &'a M,
1584 text: &'a crate::text::AnnotatedString,
1585 line_range: &Range<usize>,
1586 style: &'a TextStyle,
1587 boundary_count: usize,
1588 ) -> Self {
1589 let expected_chars = boundary_count.saturating_sub(1);
1590 let prefix_widths = measurer
1591 .measure_line_prefix_widths(text, line_range.clone(), style)
1592 .filter(|widths| widths.char_count() == expected_chars);
1593 Self {
1594 measurer,
1595 text,
1596 style,
1597 line_start: line_range.start,
1598 prefix_widths,
1599 }
1600 }
1601
1602 fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1603 if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1604 return width;
1605 }
1606 let segment_range =
1607 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1608 self.measurer
1609 .measure_subsequence(self.text, segment_range, self.style)
1610 .width
1611 }
1612
1613 fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1614 if let Some(prefix_widths) = &self.prefix_widths
1615 && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1616 {
1617 return Some(width);
1618 }
1619 None
1620 }
1621
1622 fn display_line_for_char_range(
1623 &self,
1624 boundaries: &[usize],
1625 start_idx: usize,
1626 end_idx: usize,
1627 ) -> DisplayLine {
1628 let source_range =
1629 absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1630 let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1631 DisplayLine::from_measured_source_range(source_range, measured_width)
1632 }
1633}
1634
1635#[expect(clippy::too_many_arguments)]
1638fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1639 measurer: &M,
1640 text: &crate::text::AnnotatedString,
1641 line_range: Range<usize>,
1642 style: &TextStyle,
1643 max_width: f32,
1644 line_break: LineBreak,
1645 hyphens: Hyphens,
1646 out: &mut Vec<DisplayLine>,
1647) {
1648 let line_text = &text.text[line_range.clone()];
1649 if line_text.is_empty() {
1650 out.push(DisplayLine::from_source_range(
1651 line_range.start..line_range.start,
1652 ));
1653 return;
1654 }
1655
1656 if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
1657 && measured_width <= max_width + WRAP_EPSILON
1658 {
1659 out.push(DisplayLine::from_measured_source_range(
1660 line_range,
1661 measured_width,
1662 ));
1663 return;
1664 }
1665
1666 if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1667 && line_text.chars().any(char::is_whitespace)
1668 && wrap_line_with_word_balance(
1669 measurer,
1670 text,
1671 line_range.clone(),
1672 style,
1673 max_width,
1674 line_break,
1675 out,
1676 )
1677 {
1678 return;
1679 }
1680
1681 wrap_line_greedy(
1682 measurer, text, line_range, style, max_width, line_break, hyphens, out,
1683 );
1684}
1685
1686#[expect(clippy::too_many_arguments)]
1687fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1688 measurer: &M,
1689 text: &crate::text::AnnotatedString,
1690 line_range: Range<usize>,
1691 style: &TextStyle,
1692 max_width: f32,
1693 line_break: LineBreak,
1694 hyphens: Hyphens,
1695 out: &mut Vec<DisplayLine>,
1696) {
1697 let line_text = &text.text[line_range.clone()];
1698 let boundaries = char_boundaries(line_text);
1699 let measure_context =
1700 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1701 if let Some(measured_width) =
1702 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1703 && measured_width <= max_width + WRAP_EPSILON
1704 {
1705 out.push(DisplayLine::from_measured_source_range(
1706 line_range,
1707 measured_width,
1708 ));
1709 return;
1710 }
1711 let first = out.len();
1712 let mut start_idx = 0usize;
1713
1714 while start_idx < boundaries.len() - 1 {
1715 let mut low = start_idx + 1;
1716 let mut high = boundaries.len() - 1;
1717 let mut best = start_idx + 1;
1718
1719 while low <= high {
1720 let mid = (low + high) / 2;
1721 let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1722 if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1723 best = mid;
1724 low = mid + 1;
1725 } else {
1726 if mid == 0 {
1727 break;
1728 }
1729 high = mid - 1;
1730 }
1731 }
1732
1733 let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1734 let mut effective_wrap_idx = wrap_idx;
1735 let can_hyphenate = hyphens == Hyphens::Auto
1736 && wrap_idx == best
1737 && best < boundaries.len() - 1
1738 && is_break_inside_word(line_text, &boundaries, wrap_idx);
1739 if can_hyphenate {
1740 effective_wrap_idx = resolve_auto_hyphen_break(
1741 measurer,
1742 line_text,
1743 style,
1744 &boundaries,
1745 start_idx,
1746 wrap_idx,
1747 );
1748 }
1749
1750 let broke_at_word_boundary = effective_wrap_idx > start_idx
1751 && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
1752 .chars()
1753 .all(char::is_whitespace);
1754 let segment_start = boundaries[start_idx];
1755 let mut segment_end = boundaries[effective_wrap_idx];
1756 if wrap_idx != best || broke_at_word_boundary {
1757 segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1758 }
1759 let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1760 out.push(measure_context.display_line_for_char_range(
1761 &boundaries,
1762 start_idx,
1763 segment_end_idx,
1764 ));
1765
1766 start_idx = if wrap_idx != best || broke_at_word_boundary {
1767 skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1768 } else {
1769 effective_wrap_idx
1770 };
1771 }
1772
1773 if out.len() == first {
1774 out.push(DisplayLine::from_source_range(
1775 line_range.start..line_range.start,
1776 ));
1777 }
1778}
1779
1780fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1783 measurer: &M,
1784 text: &crate::text::AnnotatedString,
1785 line_range: Range<usize>,
1786 style: &TextStyle,
1787 max_width: f32,
1788 line_break: LineBreak,
1789 out: &mut Vec<DisplayLine>,
1790) -> bool {
1791 let line_text = &text.text[line_range.clone()];
1792 let boundaries = char_boundaries(line_text);
1793 let measure_context =
1794 LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1795 if let Some(measured_width) =
1796 measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1797 && measured_width <= max_width + WRAP_EPSILON
1798 {
1799 out.push(DisplayLine::from_measured_source_range(
1800 line_range,
1801 measured_width,
1802 ));
1803 return true;
1804 }
1805 let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1806 if breakpoints.len() <= 2 {
1807 return false;
1808 }
1809
1810 let node_count = breakpoints.len();
1811 let mut best_cost = vec![f32::INFINITY; node_count];
1812 let mut next_index = vec![None; node_count];
1813 best_cost[node_count - 1] = 0.0;
1814
1815 for start in (0..node_count - 1).rev() {
1816 for end in start + 1..node_count {
1817 let start_byte = boundaries[breakpoints[start]];
1818 let end_byte = boundaries[breakpoints[end]];
1819 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1820 if trimmed_end <= start_byte {
1821 continue;
1822 }
1823 let segment_start_idx = breakpoints[start];
1824 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1825 let segment_width =
1826 measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1827 if segment_width > max_width + WRAP_EPSILON {
1828 continue;
1829 }
1830 if !best_cost[end].is_finite() {
1831 continue;
1832 }
1833 let slack = (max_width - segment_width).max(0.0);
1834 let is_last = end == node_count - 1;
1835 let segment_cost = match line_break {
1836 LineBreak::Heading => slack * slack,
1837 LineBreak::Paragraph => {
1838 if is_last {
1839 slack * slack * 0.16
1840 } else {
1841 slack * slack
1842 }
1843 }
1844 LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1845 };
1846 let candidate = segment_cost + best_cost[end];
1847 if candidate < best_cost[start] {
1848 best_cost[start] = candidate;
1849 next_index[start] = Some(end);
1850 }
1851 }
1852 }
1853
1854 let first = out.len();
1855 let mut current = 0usize;
1856 while current < node_count - 1 {
1857 let Some(next) = next_index[current] else {
1858 out.truncate(first);
1859 return false;
1860 };
1861 let start_byte = boundaries[breakpoints[current]];
1862 let end_byte = boundaries[breakpoints[next]];
1863 let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1864 if trimmed_end <= start_byte {
1865 out.truncate(first);
1866 return false;
1867 }
1868 let segment_start_idx = breakpoints[current];
1869 let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1870 out.push(measure_context.display_line_for_char_range(
1871 &boundaries,
1872 segment_start_idx,
1873 segment_end_idx,
1874 ));
1875 current = next;
1876 }
1877 out.len() > first
1878}
1879
1880fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1881 let mut points = vec![0usize];
1882 for idx in 1..boundaries.len() - 1 {
1883 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1884 let current = &line[boundaries[idx]..boundaries[idx + 1]];
1885 if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1886 points.push(idx);
1887 }
1888 }
1889 let end = boundaries.len() - 1;
1890 if points.last().copied() != Some(end) {
1891 points.push(end);
1892 }
1893 points
1894}
1895
1896fn choose_wrap_break(
1897 line: &str,
1898 boundaries: &[usize],
1899 start_idx: usize,
1900 best: usize,
1901 _line_break: LineBreak,
1902) -> usize {
1903 if best >= boundaries.len() - 1 {
1904 return best;
1905 }
1906
1907 if best <= start_idx + 1 {
1908 return best;
1909 }
1910
1911 for idx in (start_idx + 1..=best).rev() {
1912 let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1913 if prev.chars().all(char::is_whitespace) {
1914 return idx;
1915 }
1916 }
1917 best
1918}
1919
1920fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1921 if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1922 return false;
1923 }
1924 let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1925 let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1926 !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1927}
1928
1929fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1930 measurer: &M,
1931 line: &str,
1932 style: &TextStyle,
1933 boundaries: &[usize],
1934 start_idx: usize,
1935 break_idx: usize,
1936) -> usize {
1937 if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
1938 && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
1939 {
1940 return candidate;
1941 }
1942 choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1943}
1944
1945fn is_valid_auto_hyphen_break(
1946 line: &str,
1947 boundaries: &[usize],
1948 start_idx: usize,
1949 break_idx: usize,
1950 candidate_idx: usize,
1951) -> bool {
1952 let end_idx = boundaries.len().saturating_sub(1);
1953 candidate_idx > start_idx
1954 && candidate_idx < end_idx
1955 && candidate_idx <= break_idx
1956 && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1957 && is_break_inside_word(line, boundaries, candidate_idx)
1958}
1959
1960fn choose_auto_hyphen_break_fallback(
1961 boundaries: &[usize],
1962 start_idx: usize,
1963 break_idx: usize,
1964) -> usize {
1965 let end_idx = boundaries.len().saturating_sub(1);
1966 if break_idx >= end_idx {
1967 return break_idx;
1968 }
1969 let trailing_len = end_idx.saturating_sub(break_idx);
1970 if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1971 return break_idx;
1972 }
1973
1974 let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1975 let max_break = break_idx.saturating_sub(1);
1976 if min_break > max_break {
1977 return break_idx;
1978 }
1979
1980 let mut best_break = break_idx;
1981 let mut best_penalty = usize::MAX;
1982 for idx in min_break..=max_break {
1983 let candidate_trailing_len = end_idx.saturating_sub(idx);
1984 let candidate_prefix_len = idx.saturating_sub(start_idx);
1985 if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1986 || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1987 {
1988 continue;
1989 }
1990
1991 let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1992 if penalty < best_penalty {
1993 best_penalty = penalty;
1994 best_break = idx;
1995 if penalty == 0 {
1996 break;
1997 }
1998 }
1999 }
2000 best_break
2001}
2002
2003fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
2004 while idx < boundaries.len() - 1 {
2005 let ch = &line[boundaries[idx]..boundaries[idx + 1]];
2006 if !ch.chars().all(char::is_whitespace) {
2007 break;
2008 }
2009 idx += 1;
2010 }
2011 idx
2012}
2013
2014fn apply_overflow<M: TextMeasurer + ?Sized>(
2015 measurer: &M,
2016 node_id: Option<NodeId>,
2017 text: &crate::text::AnnotatedString,
2018 style: &TextStyle,
2019 options: TextLayoutOptions,
2020 max_width: Option<f32>,
2021 visible_lines: &mut Vec<DisplayLine>,
2022) -> bool {
2023 if options.overflow == TextOverflow::Visible {
2024 return false;
2025 }
2026 let ellipsis = EllipsisPlacement::for_options(options);
2027 let mut did_overflow = false;
2028 if visible_lines.len() > options.max_lines {
2029 did_overflow = true;
2030 visible_lines.truncate(options.max_lines);
2031 if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
2032 last_line.extend_to_paragraph_end(text);
2033 last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
2034 }
2035 }
2036
2037 let Some(width_limit) = max_width else {
2038 return did_overflow;
2039 };
2040 let visible_len = visible_lines.len();
2041 for (line_index, line) in visible_lines.iter_mut().enumerate() {
2042 if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2043 continue;
2044 }
2045 did_overflow = true;
2046 if line_index + 1 == visible_len
2047 && let Some(placement) = ellipsis
2048 {
2049 line.ellipsize(measurer, node_id, text, style, max_width, placement);
2050 }
2051 }
2052 did_overflow
2053}
2054
2055#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2056enum EllipsisPlacement {
2057 End,
2058 Start,
2059 Middle,
2060}
2061
2062impl EllipsisPlacement {
2063 fn split(self, kept_chars: usize) -> (usize, usize) {
2065 match self {
2066 Self::End => (kept_chars, 0),
2067 Self::Start => (0, kept_chars),
2068 Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2069 }
2070 }
2071
2072 fn estimated_kept_chars(
2077 self,
2078 prefix_widths: &TextLinePrefixWidths,
2079 ellipsis_width: f32,
2080 width_limit: f32,
2081 ) -> Option<usize> {
2082 let char_count = prefix_widths.char_count();
2083 let width = |kept_chars: usize| {
2084 let (head_chars, tail_chars) = self.split(kept_chars);
2085 Some(
2086 prefix_widths.width_for_char_range(0, head_chars)?
2087 + ellipsis_width
2088 + prefix_widths.width_for_char_range(char_count - tail_chars, char_count)?,
2089 )
2090 };
2091 let (mut fitting, mut overflowing) = (0usize, char_count + 1);
2092 while fitting + 1 < overflowing {
2093 let kept_chars = fitting + (overflowing - fitting) / 2;
2094 if width(kept_chars)? <= width_limit + WRAP_EPSILON {
2095 fitting = kept_chars;
2096 } else {
2097 overflowing = kept_chars;
2098 }
2099 }
2100 Some(fitting)
2101 }
2102
2103 fn for_options(options: TextLayoutOptions) -> Option<Self> {
2104 let single_line = options.max_lines == 1;
2105 match options.overflow {
2106 TextOverflow::Ellipsis => Some(Self::End),
2107 TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2108 TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2109 TextOverflow::StartEllipsis
2110 | TextOverflow::MiddleEllipsis
2111 | TextOverflow::Clip
2112 | TextOverflow::Visible
2113 | TextOverflow::ScaleDown { .. } => None,
2114 }
2115 }
2116
2117 fn elide(
2118 self,
2119 source: &crate::text::AnnotatedString,
2120 source_range: Range<usize>,
2121 boundaries: &[usize],
2122 kept_chars: usize,
2123 ) -> crate::text::AnnotatedString {
2124 let char_count = boundaries.len() - 1;
2125 let (head_chars, tail_chars) = self.split(kept_chars);
2126 let head_end = source_range.start + boundaries[head_chars];
2127 let tail_start = source_range.start + boundaries[char_count - tail_chars];
2128 crate::text::AnnotatedString::builder()
2129 .append_annotated_subsequence(source, source_range.start..head_end)
2130 .append(ELLIPSIS)
2131 .append_annotated_subsequence(source, tail_start..source_range.end)
2132 .to_annotated_string()
2133 }
2134}
2135
2136fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2137 measurer: &M,
2138 node_id: Option<NodeId>,
2139 source: &crate::text::AnnotatedString,
2140 source_range: Range<usize>,
2141 style: &TextStyle,
2142 max_width: Option<f32>,
2143 placement: EllipsisPlacement,
2144) -> DisplayLine {
2145 let width_limit = max_width.unwrap_or(f32::INFINITY);
2146 let fitting_line = |text: DisplayLineText| {
2147 let mut line = DisplayLine {
2148 source_range: source_range.clone(),
2149 text,
2150 measured_width: None,
2151 };
2152 let width = line.measure_width(measurer, node_id, source, style);
2153 line.measured_width = Some(width);
2154 (width <= width_limit + WRAP_EPSILON).then_some(line)
2155 };
2156 if placement != EllipsisPlacement::End
2157 && let Some(line) = fitting_line(DisplayLineText::Source)
2158 {
2159 return line;
2160 }
2161
2162 let boundaries = char_boundaries(&source.text[source_range.clone()]);
2163 let elided_line = |kept_chars: usize| {
2164 fitting_line(DisplayLineText::Ellipsized(placement.elide(
2165 source,
2166 source_range.clone(),
2167 &boundaries,
2168 kept_chars,
2169 )))
2170 };
2171 let Some(mut best) = elided_line(0) else {
2172 return DisplayLine {
2173 source_range: source_range.clone(),
2174 text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2175 measured_width: None,
2176 };
2177 };
2178
2179 let mut fitting = 0usize;
2180 let mut overflowing = boundaries.len();
2181 let guess = best.measured_width.and_then(|ellipsis_width| {
2185 measurer
2186 .measure_line_prefix_widths(source, source_range.clone(), style)
2187 .filter(|widths| widths.char_count() + 1 == boundaries.len())
2188 .and_then(|widths| placement.estimated_kept_chars(&widths, ellipsis_width, width_limit))
2189 });
2190 if let Some(guess) = guess.filter(|guess| *guess > 0) {
2191 for kept_chars in [guess, guess + 1] {
2192 if kept_chars <= fitting || kept_chars >= overflowing {
2193 break;
2194 }
2195 match elided_line(kept_chars) {
2196 Some(line) => {
2197 fitting = kept_chars;
2198 best = line;
2199 }
2200 None => overflowing = kept_chars,
2201 }
2202 }
2203 }
2204 while fitting + 1 < overflowing {
2205 let kept_chars = fitting + (overflowing - fitting) / 2;
2206 match elided_line(kept_chars) {
2207 Some(line) => {
2208 fitting = kept_chars;
2209 best = line;
2210 }
2211 None => overflowing = kept_chars,
2212 }
2213 }
2214 best
2215}
2216
2217fn char_boundaries(text: &str) -> Vec<usize> {
2218 let mut out = Vec::with_capacity(text.chars().count() + 1);
2219 out.push(0);
2220 for (idx, _) in text.char_indices() {
2221 if idx != 0 {
2222 out.push(idx);
2223 }
2224 }
2225 out.push(text.len());
2226 out
2227}
2228
2229#[cfg(test)]
2230#[path = "tests/measure_tests.rs"]
2231mod tests;