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