1use std::rc::Rc;
2
3use crate::media::text_formatting::{
4 FormattedTextSource, FormattingObjectPool, GenericTextParagraphProperties, GenericTextRunProperties,
5 ITextDrawingSink, ITextSource, InterWordJustification, TextCollapsingProperties, TextEndOfParagraph,
6 TextFormatter, TextFormatterImpl, TextLine, TextLineImpl, TextParagraphProperties, TextRun, TextRunCache,
7 TextRunProperties,
8};
9use crate::media::{
10 BaselineAlignment, CharacterHit, FlowDirection, FontFeatureCollection, IBrush, TextAlignment,
11 TextCollapsingCreateInfo, TextDecorationCollection, TextHitTestResult, TextTrimming, TextWrapping, Typeface,
12};
13use crate::utilities::{MathUtilities, ReadOnlyMemory, ValueSpan};
14use crate::{Point, Rect};
15
16#[derive(Clone)]
19pub struct TextLayoutOptions {
20 pub font_size: f64,
22 pub foreground: Option<Rc<dyn IBrush>>,
24 pub text_alignment: TextAlignment,
26 pub text_wrapping: TextWrapping,
28 pub text_trimming: Option<Rc<dyn TextTrimming>>,
30 pub text_decorations: Option<TextDecorationCollection>,
32 pub flow_direction: FlowDirection,
34 pub max_width: f64,
36 pub max_height: f64,
38 pub line_height: f64,
40 pub letter_spacing: f64,
42 pub max_lines: i32,
44 pub font_features: Option<FontFeatureCollection>,
46 pub text_style_overrides: Option<Rc<[ValueSpan<Rc<dyn TextRunProperties>>]>>,
48 pub text_run_cache: Option<Rc<TextRunCache>>,
50}
51
52impl Default for TextLayoutOptions {
53 fn default() -> Self {
54 Self {
55 font_size: GenericTextRunProperties::DEFAULT_FONT_RENDERING_EM_SIZE,
56 foreground: None,
57 text_alignment: TextAlignment::Left,
58 text_wrapping: TextWrapping::NoWrap,
59 text_trimming: None,
60 text_decorations: None,
61 flow_direction: FlowDirection::LeftToRight,
62 max_width: f64::INFINITY,
63 max_height: f64::INFINITY,
64 line_height: f64::NAN,
65 letter_spacing: 0.0,
66 max_lines: 0,
67 font_features: None,
68 text_style_overrides: None,
69 text_run_cache: None,
70 }
71 }
72}
73
74pub struct TextLayout {
76 text_source: Rc<dyn ITextSource>,
77 paragraph_properties: Rc<dyn TextParagraphProperties>,
78 text_trimming: Rc<dyn TextTrimming>,
79 text_lines: Vec<Rc<dyn TextLine>>,
80 metrics: CachedMetrics,
81 text_run_cache: Option<Rc<TextRunCache>>,
82
83 text_source_length: i32,
84
85 max_width: f64,
86 max_height: f64,
87 max_lines: i32,
88}
89
90impl TextLayout {
91 pub fn new(text: &str, typeface: Typeface, options: TextLayoutOptions) -> Self {
97 Self::from_utf16(ReadOnlyMemory::<u16>::from_str(text), typeface, options)
98 }
99
100 pub fn from_utf16(text: ReadOnlyMemory<u16>, typeface: Typeface, options: TextLayoutOptions) -> Self {
103 let paragraph_properties = Self::create_text_paragraph_properties(
104 typeface,
105 options.font_size,
106 options.foreground,
107 options.text_alignment,
108 options.text_wrapping,
109 options.text_decorations,
110 options.flow_direction,
111 options.line_height,
112 options.letter_spacing,
113 options.font_features,
114 );
115
116 let text_source: Rc<dyn ITextSource> = Rc::new(FormattedTextSource::new(
117 text,
118 paragraph_properties.default_text_run_properties().clone(),
119 options.text_style_overrides,
120 ));
121
122 Self::from_text_source(
123 text_source,
124 paragraph_properties,
125 options.text_trimming,
126 options.max_width,
127 options.max_height,
128 options.max_lines,
129 options.text_run_cache,
130 )
131 }
132
133 pub fn from_text_source(
143 text_source: Rc<dyn ITextSource>,
144 paragraph_properties: Rc<dyn TextParagraphProperties>,
145 text_trimming: Option<Rc<dyn TextTrimming>>,
146 max_width: f64,
147 max_height: f64,
148 max_lines: i32,
149 text_run_cache: Option<Rc<TextRunCache>>,
150 ) -> Self {
151 crate::perf_count!(TextLayoutsCreated);
152 let mut text_layout = Self {
153 text_source,
154 paragraph_properties,
155 text_trimming: text_trimming.unwrap_or_else(<dyn TextTrimming>::none),
156 text_lines: Vec::new(),
157 metrics: CachedMetrics::default(),
158 text_run_cache,
159 text_source_length: 0,
160 max_width,
161 max_height,
162 max_lines,
163 };
164
165 text_layout.text_lines = text_layout.create_text_lines();
166
167 text_layout
168 }
169
170 pub fn line_height(&self) -> f64 {
176 self.paragraph_properties.line_height()
177 }
178
179 pub fn max_width(&self) -> f64 {
181 self.max_width
182 }
183
184 pub fn max_height(&self) -> f64 {
186 self.max_height
187 }
188
189 pub fn max_lines(&self) -> i32 {
191 self.max_lines
192 }
193
194 pub fn letter_spacing(&self) -> f64 {
196 self.paragraph_properties.letter_spacing()
197 }
198
199 pub fn text_lines(&self) -> &[Rc<dyn TextLine>] {
201 &self.text_lines
202 }
203
204 pub fn height(&self) -> f64 {
206 self.metrics.height
207 }
208
209 pub fn extent(&self) -> f64 {
212 self.metrics.extent
213 }
214
215 pub fn baseline(&self) -> f64 {
217 self.metrics.baseline
218 }
219
220 pub fn overhang_after(&self) -> f64 {
222 self.metrics.overhang_after
223 }
224
225 pub fn overhang_leading(&self) -> f64 {
227 self.metrics.overhang_leading
228 }
229
230 pub fn overhang_trailing(&self) -> f64 {
232 self.metrics.overhang_trailing
233 }
234
235 pub fn width(&self) -> f64 {
239 self.metrics.width
240 }
241
242 pub fn width_including_trailing_whitespace(&self) -> f64 {
246 self.metrics.width_including_trailing_whitespace
247 }
248
249 pub fn draw(&self, context: &mut dyn ITextDrawingSink, origin: Point) {
254 if self.text_lines.is_empty() {
255 return;
256 }
257
258 let (current_x, mut current_y) = (origin.x, origin.y);
259
260 for text_line in &self.text_lines {
261 text_line.draw(context, Point::new(current_x, current_y));
262
263 current_y += text_line.height();
264 }
265 }
266
267 pub fn hit_test_text_position(&self, mut text_position: i32) -> Rect {
271 if self.text_lines.is_empty() {
272 return Rect::default();
273 }
274
275 if text_position < 0 {
276 text_position = self.text_source_length;
277 }
278
279 let mut current_y = 0.0;
280
281 for (i, text_line) in self.text_lines.iter().enumerate() {
282 let end = text_line.first_text_source_index() + text_line.length();
283
284 if end <= text_position && i + 1 < self.text_lines.len() {
285 current_y += text_line.height();
286
287 continue;
288 }
289
290 let character_hit = CharacterHit::new(text_position);
291
292 let start_x = text_line.get_distance_from_character_hit(character_hit);
293
294 let next_character_hit = text_line.get_next_caret_character_hit(character_hit);
295
296 let end_x = text_line.get_distance_from_character_hit(next_character_hit);
297
298 return Rect::new(start_x, current_y, end_x - start_x, text_line.height());
299 }
300
301 Rect::default()
302 }
303
304 pub fn hit_test_text_range(&self, mut start: i32, mut length: i32) -> Vec<Rect> {
307 if start + length <= 0 {
308 return Vec::new();
309 }
310
311 let mut result: Vec<Rect> = Vec::with_capacity(self.text_lines.len());
312
313 let mut current_y = 0.0;
314
315 for text_line in &self.text_lines {
316 if text_line.first_text_source_index() + text_line.length() <= start {
318 current_y += text_line.height();
319
320 continue;
321 }
322
323 let text_bounds = text_line.get_text_bounds(start, length);
324
325 for bounds in &text_bounds {
326 let rectangle = bounds.rectangle();
327
328 match result.last_mut() {
329 Some(last)
330 if MathUtilities::are_close(last.right(), rectangle.left())
331 && MathUtilities::are_close(last.top(), current_y) =>
332 {
333 *last = last.with_width(last.width + rectangle.width);
334 }
335 _ => {
336 result.push(rectangle.with_y(current_y));
337 }
338 }
339
340 for run_bounds in bounds.text_run_bounds() {
341 start += run_bounds.length();
342 length -= run_bounds.length();
343 }
344 }
345
346 if text_line.first_text_source_index() + text_line.length() >= start + length {
347 break;
348 }
349
350 current_y += text_line.height();
351 }
352
353 result
354 }
355
356 pub fn hit_test_point(&self, point: Point) -> TextHitTestResult {
358 let mut current_y = 0.0;
359
360 let mut current_line: Option<&Rc<dyn TextLine>> = None;
361
362 for text_line in &self.text_lines {
363 current_line = Some(text_line);
364
365 if current_y + text_line.height() > point.y {
366 let character_hit = text_line.get_character_hit_from_distance(point.x);
367
368 return self.get_hit_test_result(&**text_line, character_hit, point);
369 }
370
371 current_y += text_line.height();
372 }
373
374 let Some(current_line) = current_line else {
375 return TextHitTestResult::default();
376 };
377
378 let character_hit = current_line.get_character_hit_from_distance(point.x);
379
380 self.get_hit_test_result(&**current_line, character_hit, point)
381 }
382
383 pub fn get_line_index_from_character_index(&self, char_index: i32, trailing_edge: bool) -> i32 {
385 if char_index < 0 {
386 return 0;
387 }
388
389 if char_index > self.text_source_length {
390 return self.text_lines.len() as i32 - 1;
391 }
392
393 for (index, text_line) in self.text_lines.iter().enumerate() {
394 if text_line.first_text_source_index() + text_line.length() < char_index {
395 continue;
396 }
397
398 if char_index >= text_line.first_text_source_index()
399 && char_index
400 <= text_line.first_text_source_index() + text_line.length() - if trailing_edge { 0 } else { 1 }
401 {
402 return index as i32;
403 }
404 }
405
406 self.text_lines.len() as i32 - 1
407 }
408
409 fn get_hit_test_result(
410 &self,
411 text_line: &dyn TextLine,
412 mut character_hit: CharacterHit,
413 point: Point,
414 ) -> TextHitTestResult {
415 let (x, y) = (point.x, point.y);
416
417 let is_inside = x >= 0.0 && x <= text_line.width() && y >= 0.0 && y <= text_line.height();
418
419 let mut last_trailing_index = 0;
420
421 let text_end_of_line_length = text_line.text_line_break().and_then(|text_line_break| {
422 text_line_break
423 .text_end_of_line()
424 .and_then(|text_end_of_line| text_end_of_line.as_text_end_of_line())
425 .map(|text_end_of_line| text_end_of_line.length())
426 });
427
428 if self.paragraph_properties.flow_direction() == FlowDirection::LeftToRight {
429 last_trailing_index = text_line.first_text_source_index() + text_line.length();
430
431 if x >= text_line.width() && text_line.length() > 0 && text_line.new_line_length() > 0 {
432 last_trailing_index -= text_line.new_line_length();
433 }
434
435 if let Some(text_end_of_line_length) = text_end_of_line_length {
436 last_trailing_index -= text_end_of_line_length;
437 }
438 } else {
439 if x <= text_line.width_including_trailing_whitespace() - text_line.width()
440 && text_line.length() > 0
441 && text_line.new_line_length() > 0
442 {
443 last_trailing_index += text_line.new_line_length();
444 }
445
446 if let Some(text_end_of_line_length) = text_end_of_line_length {
447 last_trailing_index += text_end_of_line_length;
448 }
449 }
450
451 let mut text_position = character_hit.first_character_index() + character_hit.trailing_length();
452
453 let is_trailing =
454 last_trailing_index == text_position && character_hit.trailing_length() > 0 || y > self.height();
455
456 if text_position == text_line.first_text_source_index() + text_line.length() {
457 text_position -= text_line.new_line_length();
458 }
459
460 if text_line.new_line_length() > 0
461 && text_position + text_line.new_line_length()
462 == character_hit.first_character_index() + character_hit.trailing_length()
463 {
464 character_hit = CharacterHit::new(character_hit.first_character_index());
465 }
466
467 TextHitTestResult::new(character_hit, text_position, is_inside, is_trailing)
468 }
469
470 #[allow(clippy::too_many_arguments)]
483 pub(crate) fn create_text_paragraph_properties(
484 typeface: Typeface,
485 font_size: f64,
486 foreground: Option<Rc<dyn IBrush>>,
487 text_alignment: TextAlignment,
488 text_wrapping: TextWrapping,
489 text_decorations: Option<TextDecorationCollection>,
490 flow_direction: FlowDirection,
491 line_height: f64,
492 letter_spacing: f64,
493 features: Option<FontFeatureCollection>,
494 ) -> Rc<dyn TextParagraphProperties> {
495 let text_run_style: Rc<dyn TextRunProperties> = Rc::new(GenericTextRunProperties::with_all(
496 typeface,
497 font_size,
498 text_decorations,
499 foreground,
500 None,
501 BaselineAlignment::Baseline,
502 None,
503 features,
504 ));
505
506 Rc::new(GenericTextParagraphProperties::with_all(
507 flow_direction,
508 text_alignment,
509 true,
510 false,
511 text_run_style,
512 text_wrapping,
513 line_height,
514 0.0,
515 letter_spacing,
516 ))
517 }
518
519 fn create_text_lines(&mut self) -> Vec<Rc<dyn TextLine>> {
520 let object_pool = FormattingObjectPool::instance();
521
522 let mut first = true;
523
524 if MathUtilities::is_zero(self.max_width) || MathUtilities::is_zero(self.max_height) {
525 let text_line: Rc<dyn TextLine> =
526 TextFormatterImpl::create_empty_text_line(0, f64::INFINITY, &self.paragraph_properties);
527
528 self.update_metrics(&*text_line, &mut first);
529
530 return vec![text_line];
531 }
532
533 let mut text_lines = object_pool.text_lines.rent();
534
535 self.text_source_length = 0;
536
537 let mut previous_line: Option<Rc<dyn TextLine>> = None;
538
539 let text_formatter = <dyn TextFormatter>::current();
540
541 loop {
542 let previous_line_break = previous_line.as_ref().and_then(|line| line.text_line_break());
543
544 let text_line = text_formatter
546 .format_line_with_cache(
547 &*self.text_source,
548 self.text_source_length,
549 self.max_width,
550 &self.paragraph_properties,
551 previous_line_break.as_ref(),
552 self.text_run_cache.as_deref(),
553 )
554 .filter(|text_line| text_line.as_any().is::<TextLineImpl>());
555
556 let Some(mut text_line) = text_line else {
557 if previous_line.as_ref().is_some_and(|previous_line| previous_line.new_line_length() > 0) {
558 let empty_text_line: Rc<dyn TextLine> = TextFormatterImpl::create_empty_text_line(
559 self.text_source_length,
560 self.max_width,
561 &self.paragraph_properties,
562 );
563
564 text_lines.push(empty_text_line.clone());
565
566 self.update_metrics(&*empty_text_line, &mut first);
567 }
568
569 break;
570 };
571
572 self.text_source_length += text_line.length();
573
574 let has_real_content = text_line.length() > text_line.new_line_length();
578
579 if !text_lines.is_empty()
585 && self.max_height != f64::INFINITY
586 && MathUtilities::greater_than(self.height() + text_line.height(), self.max_height)
587 {
588 let last = text_lines.len() - 1;
589 let last_line = text_lines[last].clone();
590
591 if !self.is_trimming_none()
592 && has_real_content
593 && last_line.text_line_break().is_some_and(|text_line_break| text_line_break.is_split())
594 {
595 text_lines[last] = self.collapse_for_truncation(last_line);
596 }
597
598 break;
599 }
600
601 if self.max_lines > 0 && text_lines.len() as i32 >= self.max_lines {
605 if !self.is_trimming_none() && has_real_content {
606 let last = text_lines.len() - 1;
607 let last_line = text_lines[last].clone();
608
609 text_lines[last] = self.collapse_for_truncation(last_line);
610 }
611
612 break;
613 }
614
615 let has_overflowed = text_line.has_overflowed();
616
617 if has_overflowed && !self.is_trimming_none() {
618 text_line = text_line.collapse(&[self.get_collapsing_properties(self.max_width)]);
619 }
620
621 text_lines.push(text_line.clone());
622
623 self.update_metrics(&*text_line, &mut first);
624
625 previous_line = Some(text_line.clone());
626
627 let is_end_of_paragraph = text_line.text_line_break().is_some_and(|text_line_break| {
628 text_line_break
629 .text_end_of_line()
630 .is_some_and(|text_end_of_line| text_end_of_line.is::<TextEndOfParagraph>())
631 });
632
633 if is_end_of_paragraph {
634 break;
635 }
636 }
637
638 if text_lines.is_empty() {
639 let text_line: Rc<dyn TextLine> =
640 TextFormatterImpl::create_empty_text_line(0, self.max_width, &self.paragraph_properties);
641
642 text_lines.push(text_line.clone());
643
644 self.update_metrics(&*text_line, &mut first);
645 }
646
647 if self.paragraph_properties.text_alignment() == TextAlignment::Justify {
648 let justification_width = self.max_width;
654
655 if !justification_width.is_infinite() && justification_width > 0.0 {
656 let justification_properties = InterWordJustification::new(justification_width);
657
658 for (i, line) in text_lines.iter().enumerate() {
659 if i == text_lines.len() - 1
665 || line.new_line_length() > 0
666 || line
667 .text_line_break()
668 .is_some_and(|text_line_break| text_line_break.text_end_of_line().is_some())
669 {
670 continue;
671 }
672
673 line.justify(&justification_properties);
674 }
675 }
676 }
677
678 let result = text_lines.to_vec();
679
680 object_pool.text_lines.return_list(text_lines);
681
682 result
683 }
684
685 fn update_metrics(&mut self, current_line: &dyn TextLine, first: &mut bool) {
686 let metrics = &mut self.metrics;
687
688 metrics.height += current_line.height();
690
691 metrics.width = metrics.width.max(current_line.width());
694
695 metrics.extent = metrics.extent.max(current_line.extent());
697
698 let previous_text_width = metrics.width_including_trailing_whitespace;
703 let text_width = current_line.width_including_trailing_whitespace();
704
705 if previous_text_width < text_width {
706 metrics.width_including_trailing_whitespace = current_line.width_including_trailing_whitespace();
707 metrics.overhang_leading = current_line.overhang_leading();
708 metrics.overhang_trailing = current_line.overhang_trailing();
709 }
710
711 metrics.overhang_after = current_line.overhang_after();
713
714 if *first {
716 metrics.baseline = current_line.baseline();
717 *first = false;
718 }
719 }
720
721 fn is_trimming_none(&self) -> bool {
723 Rc::ptr_eq(&self.text_trimming, &<dyn TextTrimming>::none())
724 }
725
726 fn get_collapsing_properties(&self, width: f64) -> Option<Rc<dyn TextCollapsingProperties>> {
730 if self.is_trimming_none() {
731 return None;
732 }
733
734 Some(self.text_trimming.create_collapsing_properties(&TextCollapsingCreateInfo::new(
735 width,
736 self.paragraph_properties.default_text_run_properties().clone(),
737 self.paragraph_properties.flow_direction(),
738 )))
739 }
740
741 fn collapse_for_truncation(&self, line: Rc<dyn TextLine>) -> Rc<dyn TextLine> {
742 let collapsing_properties = self.get_collapsing_properties(line.width_including_trailing_whitespace());
746
747 if collapsing_properties.is_none() {
748 return line;
749 }
750
751 line.collapse(&[collapsing_properties])
752 }
753
754 pub fn dispose(&self) {
756 for line in &self.text_lines {
757 line.dispose();
758 }
759 }
760}
761
762#[derive(Default)]
763struct CachedMetrics {
764 height: f64,
766 baseline: f64,
767
768 width: f64,
770 width_including_trailing_whitespace: f64,
771
772 extent: f64,
774 overhang_after: f64,
775
776 overhang_leading: f64,
778 overhang_trailing: f64,
779}