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