1pub mod error;
2
3pub use error::*;
4
5use graphitepdf_font::{FontDescriptor, FontSource, FontStore, StandardFont};
6use graphitepdf_primitives::Pt;
7use std::cmp::Ordering;
8
9#[derive(Clone, Debug, PartialEq)]
10pub struct TextSpan {
11 content: String,
12 font: Option<FontDescriptor>,
13 font_size: Pt,
14}
15
16impl TextSpan {
17 pub fn new(content: impl Into<String>) -> Result<Self> {
18 let content = content.into();
19 if content.trim().is_empty() {
20 return Err(Error::EmptyText);
21 }
22
23 Ok(Self {
24 content,
25 font: None,
26 font_size: Pt::new(12.0),
27 })
28 }
29
30 pub fn content(&self) -> &str {
31 &self.content
32 }
33
34 pub fn font(&self) -> Option<&FontDescriptor> {
35 self.font.as_ref()
36 }
37
38 pub const fn font_size(&self) -> Pt {
39 self.font_size
40 }
41
42 pub fn with_font(mut self, font: FontDescriptor) -> Self {
43 self.font = Some(font);
44 self
45 }
46
47 pub fn with_font_size(mut self, font_size: Pt) -> Result<Self> {
48 if font_size.value() <= 0.0 {
49 return Err(Error::InvalidFontSize {
50 size: font_size.value(),
51 });
52 }
53
54 self.font_size = font_size;
55 Ok(self)
56 }
57}
58
59#[derive(Clone, Debug, Default, PartialEq)]
60pub struct TextBlock {
61 spans: Vec<TextSpan>,
62}
63
64impl TextBlock {
65 pub fn new(spans: impl IntoIterator<Item = TextSpan>) -> Self {
66 Self {
67 spans: spans.into_iter().collect(),
68 }
69 }
70
71 pub fn push(&mut self, span: TextSpan) {
72 self.spans.push(span);
73 }
74
75 pub fn spans(&self) -> &[TextSpan] {
76 &self.spans
77 }
78
79 pub fn plain_text(&self) -> String {
80 self.spans
81 .iter()
82 .map(TextSpan::content)
83 .collect::<Vec<_>>()
84 .join("")
85 }
86
87 pub fn is_empty(&self) -> bool {
88 self.spans.is_empty()
89 }
90}
91
92impl From<TextSpan> for TextBlock {
93 fn from(value: TextSpan) -> Self {
94 Self { spans: vec![value] }
95 }
96}
97
98impl TextBlock {
99 pub fn to_attributed_string(&self) -> Result<AttributedString> {
100 AttributedString::try_from(self)
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub struct TextRange {
106 start: usize,
107 end: usize,
108}
109
110impl TextRange {
111 pub const fn new(start: usize, end: usize) -> Self {
112 Self { start, end }
113 }
114
115 pub const fn start(self) -> usize {
116 self.start
117 }
118
119 pub const fn end(self) -> usize {
120 self.end
121 }
122
123 pub const fn len(self) -> usize {
124 self.end.saturating_sub(self.start)
125 }
126
127 pub const fn is_empty(self) -> bool {
128 self.start >= self.end
129 }
130
131 fn validate_for(self, text: &str) -> Result<()> {
132 if self.start > self.end || self.end > text.len() {
133 return Err(Error::InvalidTextRange {
134 start: self.start,
135 end: self.end,
136 len: text.len(),
137 });
138 }
139
140 if !text.is_char_boundary(self.start) || !text.is_char_boundary(self.end) {
141 return Err(Error::NonCharacterBoundaryRange {
142 start: self.start,
143 end: self.end,
144 });
145 }
146
147 Ok(())
148 }
149}
150
151#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
152pub enum TextDirection {
153 #[default]
154 Ltr,
155 Rtl,
156}
157
158#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
159pub enum Script {
160 #[default]
161 Common,
162 Latin,
163 Arabic,
164 Hebrew,
165 Cyrillic,
166 Han,
167 Unknown,
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub enum TextDecorationKind {
172 Underline,
173 Overline,
174 LineThrough,
175}
176
177#[derive(Clone, Debug, PartialEq)]
178pub struct TextDecoration {
179 kind: TextDecorationKind,
180 thickness: Option<Pt>,
181 offset: Option<Pt>,
182}
183
184impl TextDecoration {
185 pub const fn new(kind: TextDecorationKind) -> Self {
186 Self {
187 kind,
188 thickness: None,
189 offset: None,
190 }
191 }
192
193 pub const fn kind(&self) -> TextDecorationKind {
194 self.kind
195 }
196
197 pub const fn thickness(&self) -> Option<Pt> {
198 self.thickness
199 }
200
201 pub const fn offset(&self) -> Option<Pt> {
202 self.offset
203 }
204
205 pub fn with_thickness(mut self, thickness: Pt) -> Self {
206 self.thickness = Some(thickness);
207 self
208 }
209
210 pub fn with_offset(mut self, offset: Pt) -> Self {
211 self.offset = Some(offset);
212 self
213 }
214}
215
216#[derive(Clone, Debug, PartialEq)]
217pub struct TextAttributes {
218 font: Option<FontDescriptor>,
219 font_size: Pt,
220 letter_spacing: Pt,
221 language: Option<String>,
222 direction: Option<TextDirection>,
223 decorations: Vec<TextDecoration>,
224}
225
226impl Default for TextAttributes {
227 fn default() -> Self {
228 Self {
229 font: None,
230 font_size: Pt::new(12.0),
231 letter_spacing: Pt::zero(),
232 language: None,
233 direction: None,
234 decorations: Vec::new(),
235 }
236 }
237}
238
239impl TextAttributes {
240 pub fn font(&self) -> Option<&FontDescriptor> {
241 self.font.as_ref()
242 }
243
244 pub const fn font_size(&self) -> Pt {
245 self.font_size
246 }
247
248 pub const fn letter_spacing(&self) -> Pt {
249 self.letter_spacing
250 }
251
252 pub fn language(&self) -> Option<&str> {
253 self.language.as_deref()
254 }
255
256 pub const fn direction(&self) -> Option<TextDirection> {
257 self.direction
258 }
259
260 pub fn decorations(&self) -> &[TextDecoration] {
261 &self.decorations
262 }
263
264 pub fn with_font(mut self, font: FontDescriptor) -> Self {
265 self.font = Some(font);
266 self
267 }
268
269 pub fn with_font_size(mut self, font_size: Pt) -> Result<Self> {
270 if font_size.value() <= 0.0 {
271 return Err(Error::InvalidFontSize {
272 size: font_size.value(),
273 });
274 }
275
276 self.font_size = font_size;
277 Ok(self)
278 }
279
280 pub fn with_letter_spacing(mut self, letter_spacing: Pt) -> Self {
281 self.letter_spacing = letter_spacing;
282 self
283 }
284
285 pub fn with_language(mut self, language: impl Into<String>) -> Self {
286 self.language = Some(language.into());
287 self
288 }
289
290 pub fn with_direction(mut self, direction: TextDirection) -> Self {
291 self.direction = Some(direction);
292 self
293 }
294
295 pub fn with_decoration(mut self, decoration: TextDecoration) -> Self {
296 self.decorations.push(decoration);
297 self
298 }
299}
300
301impl From<&TextSpan> for TextAttributes {
302 fn from(value: &TextSpan) -> Self {
303 let mut attributes = TextAttributes::default();
304 if let Some(font) = value.font.clone() {
305 attributes = attributes.with_font(font);
306 }
307 attributes.font_size = value.font_size;
308 attributes
309 }
310}
311
312#[derive(Clone, Debug, PartialEq)]
313pub struct AttributeRun {
314 range: TextRange,
315 attributes: TextAttributes,
316}
317
318impl AttributeRun {
319 pub const fn new(range: TextRange, attributes: TextAttributes) -> Self {
320 Self { range, attributes }
321 }
322
323 pub const fn range(&self) -> TextRange {
324 self.range
325 }
326
327 pub const fn attributes(&self) -> &TextAttributes {
328 &self.attributes
329 }
330}
331
332#[derive(Clone, Debug, PartialEq)]
333pub struct AttributedString {
334 text: String,
335 default_attributes: TextAttributes,
336 runs: Vec<AttributeRun>,
337}
338
339impl AttributedString {
340 pub fn new(text: impl Into<String>) -> Result<Self> {
341 let text = text.into();
342 if text.is_empty() {
343 return Err(Error::EmptyText);
344 }
345
346 Ok(Self {
347 text,
348 default_attributes: TextAttributes::default(),
349 runs: Vec::new(),
350 })
351 }
352
353 pub fn text(&self) -> &str {
354 &self.text
355 }
356
357 pub fn default_attributes(&self) -> &TextAttributes {
358 &self.default_attributes
359 }
360
361 pub fn runs(&self) -> &[AttributeRun] {
362 &self.runs
363 }
364
365 pub fn with_default_attributes(mut self, attributes: TextAttributes) -> Result<Self> {
366 validate_font_size(attributes.font_size())?;
367 self.default_attributes = attributes;
368 Ok(self)
369 }
370
371 pub fn push_run(&mut self, range: TextRange, attributes: TextAttributes) -> Result<()> {
372 range.validate_for(&self.text)?;
373 validate_font_size(attributes.font_size())?;
374 self.runs.push(AttributeRun::new(range, attributes));
375 Ok(())
376 }
377
378 pub fn with_run(mut self, range: TextRange, attributes: TextAttributes) -> Result<Self> {
379 self.push_run(range, attributes)?;
380 Ok(self)
381 }
382
383 fn canonical_runs(&self) -> Result<Vec<CanonicalRun>> {
384 let mut boundaries = vec![0, self.text.len()];
385 for run in &self.runs {
386 run.range.validate_for(&self.text)?;
387 boundaries.push(run.range.start());
388 boundaries.push(run.range.end());
389 }
390 boundaries.sort_unstable();
391 boundaries.dedup();
392
393 let mut runs = Vec::new();
394 for window in boundaries.windows(2) {
395 let range = TextRange::new(window[0], window[1]);
396 if range.is_empty() {
397 continue;
398 }
399
400 let mut attributes = self.default_attributes.clone();
401 for run in &self.runs {
402 if run.range.start() <= range.start() && range.end() <= run.range.end() {
403 attributes = run.attributes.clone();
404 }
405 }
406
407 let text = self.text[range.start()..range.end()].to_string();
408 runs.push(CanonicalRun {
409 range,
410 text,
411 attributes,
412 });
413 }
414
415 Ok(runs)
416 }
417}
418
419impl TryFrom<&TextBlock> for AttributedString {
420 type Error = Error;
421
422 fn try_from(value: &TextBlock) -> Result<Self> {
423 let text = value.plain_text();
424 if text.is_empty() {
425 return Err(Error::EmptyText);
426 }
427
428 let mut attributed = AttributedString::new(text)?;
429 let mut start = 0;
430 for span in value.spans() {
431 let end = start + span.content().len();
432 attributed.push_run(TextRange::new(start, end), TextAttributes::from(span))?;
433 start = end;
434 }
435 Ok(attributed)
436 }
437}
438
439impl TryFrom<TextBlock> for AttributedString {
440 type Error = Error;
441
442 fn try_from(value: TextBlock) -> Result<Self> {
443 Self::try_from(&value)
444 }
445}
446
447#[derive(Clone, Copy, Debug, Default, PartialEq)]
448pub struct TextRect {
449 pub x: Pt,
450 pub y: Pt,
451 pub width: Pt,
452 pub height: Pt,
453}
454
455impl TextRect {
456 pub const fn new(x: Pt, y: Pt, width: Pt, height: Pt) -> Self {
457 Self {
458 x,
459 y,
460 width,
461 height,
462 }
463 }
464
465 pub const fn from_values(x: f32, y: f32, width: f32, height: f32) -> Self {
466 Self::new(Pt::new(x), Pt::new(y), Pt::new(width), Pt::new(height))
467 }
468
469 pub const fn right(&self) -> Pt {
470 Pt::new(self.x.value() + self.width.value())
471 }
472
473 pub const fn bottom(&self) -> Pt {
474 Pt::new(self.y.value() + self.height.value())
475 }
476}
477
478#[derive(Clone, Copy, Debug, PartialEq)]
479pub struct TextContainer {
480 rect: TextRect,
481 max_lines: Option<usize>,
482}
483
484impl TextContainer {
485 pub fn new(rect: TextRect) -> Result<Self> {
486 if rect.width.value() <= 0.0 || rect.height.value() <= 0.0 {
487 return Err(Error::InvalidTextContainer {
488 width: rect.width.value(),
489 height: rect.height.value(),
490 });
491 }
492
493 Ok(Self {
494 rect,
495 max_lines: None,
496 })
497 }
498
499 pub const fn rect(&self) -> TextRect {
500 self.rect
501 }
502
503 pub const fn max_lines(&self) -> Option<usize> {
504 self.max_lines
505 }
506
507 pub fn with_max_lines(mut self, max_lines: usize) -> Self {
508 self.max_lines = Some(max_lines);
509 self
510 }
511}
512
513#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
514pub enum BidiMode {
515 #[default]
516 Auto,
517 LeftToRight,
518 RightToLeft,
519}
520
521#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
522pub enum LineBreaking {
523 #[default]
524 WordBoundary,
525 CharacterBoundary,
526 WordBoundaryOrCharacter,
527}
528
529#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
530pub enum Justification {
531 #[default]
532 Start,
533 End,
534 Center,
535 Full,
536}
537
538#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
539pub enum ScriptItemization {
540 None,
541 #[default]
542 Heuristic,
543}
544
545#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
546pub enum TextDecorationMode {
547 Suppress,
548 #[default]
549 Preserve,
550}
551
552#[derive(Clone, Debug, PartialEq, Default)]
553pub enum FontSubstitution {
554 Disabled,
555 FallbackFamilies(Vec<FontDescriptor>),
556 #[default]
557 BestEffort,
558}
559
560#[derive(Clone, Debug, PartialEq)]
561pub enum WordHyphenation {
562 Disabled,
563 Enabled { min_word_chars: usize, marker: char },
564}
565
566impl Default for WordHyphenation {
567 fn default() -> Self {
568 Self::Enabled {
569 min_word_chars: 8,
570 marker: '-',
571 }
572 }
573}
574
575#[derive(Clone, Debug, PartialEq)]
576pub struct TextEngineConfig {
577 pub bidi: BidiMode,
578 pub line_breaking: LineBreaking,
579 pub justification: Justification,
580 pub font_substitution: FontSubstitution,
581 pub script_itemization: ScriptItemization,
582 pub text_decoration_mode: TextDecorationMode,
583 pub word_hyphenation: WordHyphenation,
584 pub default_font: FontDescriptor,
585}
586
587impl Default for TextEngineConfig {
588 fn default() -> Self {
589 Self {
590 bidi: BidiMode::Auto,
591 line_breaking: LineBreaking::WordBoundaryOrCharacter,
592 justification: Justification::Start,
593 font_substitution: FontSubstitution::BestEffort,
594 script_itemization: ScriptItemization::Heuristic,
595 text_decoration_mode: TextDecorationMode::Preserve,
596 word_hyphenation: WordHyphenation::default(),
597 default_font: FontDescriptor::new(StandardFont::Helvetica.family_name()),
598 }
599 }
600}
601
602#[derive(Clone, Debug, PartialEq)]
603pub struct TextRun {
604 range: TextRange,
605 text: String,
606 requested_font: Option<FontDescriptor>,
607 resolved_font: FontDescriptor,
608 font_source: Option<FontSource>,
609 font_size: Pt,
610 letter_spacing: Pt,
611 language: Option<String>,
612 direction: TextDirection,
613 script: Script,
614 decorations: Vec<TextDecoration>,
615}
616
617impl TextRun {
618 pub const fn range(&self) -> TextRange {
619 self.range
620 }
621
622 pub fn text(&self) -> &str {
623 &self.text
624 }
625
626 pub fn requested_font(&self) -> Option<&FontDescriptor> {
627 self.requested_font.as_ref()
628 }
629
630 pub const fn resolved_font(&self) -> &FontDescriptor {
631 &self.resolved_font
632 }
633
634 pub const fn font_source(&self) -> Option<&FontSource> {
635 self.font_source.as_ref()
636 }
637
638 pub const fn font_size(&self) -> Pt {
639 self.font_size
640 }
641
642 pub const fn letter_spacing(&self) -> Pt {
643 self.letter_spacing
644 }
645
646 pub fn language(&self) -> Option<&str> {
647 self.language.as_deref()
648 }
649
650 pub const fn direction(&self) -> TextDirection {
651 self.direction
652 }
653
654 pub const fn script(&self) -> Script {
655 self.script
656 }
657
658 pub fn decorations(&self) -> &[TextDecoration] {
659 &self.decorations
660 }
661}
662
663#[derive(Clone, Debug, PartialEq)]
664pub struct TextFragment {
665 range: TextRange,
666 text: String,
667 rect: TextRect,
668 baseline: Pt,
669 direction: TextDirection,
670 script: Script,
671 font: FontDescriptor,
672 font_size: Pt,
673 decorations: Vec<TextDecoration>,
674 inserted_hyphen: bool,
675 whitespace: bool,
676}
677
678impl TextFragment {
679 pub const fn range(&self) -> TextRange {
680 self.range
681 }
682
683 pub fn text(&self) -> &str {
684 &self.text
685 }
686
687 pub const fn rect(&self) -> TextRect {
688 self.rect
689 }
690
691 pub const fn baseline(&self) -> Pt {
692 self.baseline
693 }
694
695 pub const fn direction(&self) -> TextDirection {
696 self.direction
697 }
698
699 pub const fn script(&self) -> Script {
700 self.script
701 }
702
703 pub const fn font(&self) -> &FontDescriptor {
704 &self.font
705 }
706
707 pub const fn font_size(&self) -> Pt {
708 self.font_size
709 }
710
711 pub fn decorations(&self) -> &[TextDecoration] {
712 &self.decorations
713 }
714
715 pub const fn inserted_hyphen(&self) -> bool {
716 self.inserted_hyphen
717 }
718
719 pub const fn is_whitespace(&self) -> bool {
720 self.whitespace
721 }
722}
723
724#[derive(Clone, Debug, PartialEq)]
725pub struct LineFragment {
726 rect: TextRect,
727 baseline: Pt,
728 direction: TextDirection,
729 justification: Justification,
730 fragments: Vec<TextFragment>,
731}
732
733impl LineFragment {
734 pub const fn rect(&self) -> TextRect {
735 self.rect
736 }
737
738 pub const fn baseline(&self) -> Pt {
739 self.baseline
740 }
741
742 pub const fn direction(&self) -> TextDirection {
743 self.direction
744 }
745
746 pub const fn justification(&self) -> Justification {
747 self.justification
748 }
749
750 pub fn fragments(&self) -> &[TextFragment] {
751 &self.fragments
752 }
753}
754
755#[derive(Clone, Debug, PartialEq)]
756pub struct TextLayout {
757 lines: Vec<LineFragment>,
758 runs: Vec<TextRun>,
759 bounds: TextRect,
760 overflowed: bool,
761}
762
763impl TextLayout {
764 pub fn lines(&self) -> &[LineFragment] {
765 &self.lines
766 }
767
768 pub fn runs(&self) -> &[TextRun] {
769 &self.runs
770 }
771
772 pub const fn bounds(&self) -> TextRect {
773 self.bounds
774 }
775
776 pub const fn overflowed(&self) -> bool {
777 self.overflowed
778 }
779
780 pub fn fragments(&self) -> impl Iterator<Item = &TextFragment> {
781 self.lines.iter().flat_map(|line| line.fragments.iter())
782 }
783}
784
785#[derive(Clone, Debug, Default)]
786pub struct TextEngine {
787 config: TextEngineConfig,
788}
789
790impl TextEngine {
791 pub fn new(config: TextEngineConfig) -> Self {
792 Self { config }
793 }
794
795 pub fn config(&self) -> &TextEngineConfig {
796 &self.config
797 }
798
799 pub fn layout(
800 &self,
801 attributed: &AttributedString,
802 container: &TextContainer,
803 font_store: Option<&FontStore>,
804 ) -> Result<TextLayout> {
805 let runs = self.resolve_runs(attributed, font_store)?;
806 self.layout_runs(runs, *container, font_store)
807 }
808
809 pub fn layout_text_block(
810 &self,
811 block: &TextBlock,
812 container: &TextContainer,
813 font_store: Option<&FontStore>,
814 ) -> Result<TextLayout> {
815 let attributed = block.to_attributed_string()?;
816 self.layout(&attributed, container, font_store)
817 }
818
819 fn resolve_runs(
820 &self,
821 attributed: &AttributedString,
822 font_store: Option<&FontStore>,
823 ) -> Result<Vec<TextRun>> {
824 let mut resolved = Vec::new();
825
826 for run in attributed.canonical_runs()? {
827 match self.config.script_itemization {
828 ScriptItemization::None => {
829 resolved.push(self.build_text_run(
830 run.range,
831 run.text,
832 run.attributes,
833 font_store,
834 )?);
835 }
836 ScriptItemization::Heuristic => {
837 for item in self.itemize_run(run.range, &run.text, &run.attributes) {
838 resolved.push(self.build_text_run(
839 item.range,
840 item.text,
841 run.attributes.clone(),
842 font_store,
843 )?);
844 }
845 }
846 }
847 }
848
849 Ok(resolved)
850 }
851
852 fn build_text_run(
853 &self,
854 range: TextRange,
855 text: String,
856 attributes: TextAttributes,
857 font_store: Option<&FontStore>,
858 ) -> Result<TextRun> {
859 let requested_font = attributes.font().cloned();
860 let (resolved_font, font_source) = self.resolve_font(attributes.font(), font_store)?;
861 let direction = resolve_direction(attributes.direction(), self.config.bidi, &text);
862 let script = detect_script_in_text(&text);
863 let decorations = match self.config.text_decoration_mode {
864 TextDecorationMode::Preserve => attributes.decorations.clone(),
865 TextDecorationMode::Suppress => Vec::new(),
866 };
867
868 Ok(TextRun {
869 range,
870 text,
871 requested_font,
872 resolved_font,
873 font_source,
874 font_size: attributes.font_size(),
875 letter_spacing: attributes.letter_spacing(),
876 language: attributes.language().map(ToOwned::to_owned),
877 direction,
878 script,
879 decorations,
880 })
881 }
882
883 fn resolve_font(
884 &self,
885 requested: Option<&FontDescriptor>,
886 font_store: Option<&FontStore>,
887 ) -> Result<(FontDescriptor, Option<FontSource>)> {
888 let requested = requested
889 .cloned()
890 .unwrap_or_else(|| self.config.default_font.clone());
891
892 let Some(font_store) = font_store else {
893 return Ok((requested, None));
894 };
895
896 if let Ok(registered) = font_store.get_font(&requested) {
897 return Ok((
898 registered.descriptor().clone(),
899 Some(registered.source().clone()),
900 ));
901 }
902
903 let mut fallbacks = Vec::new();
904 match &self.config.font_substitution {
905 FontSubstitution::Disabled => {}
906 FontSubstitution::FallbackFamilies(descriptors) => {
907 fallbacks.extend(
908 descriptors
909 .iter()
910 .cloned()
911 .map(|descriptor| harmonize_descriptor(descriptor, &requested)),
912 );
913 }
914 FontSubstitution::BestEffort => {
915 fallbacks.push(harmonize_descriptor(
916 self.config.default_font.clone(),
917 &requested,
918 ));
919 fallbacks.push(harmonize_descriptor(
920 FontDescriptor::new(StandardFont::Helvetica.family_name()),
921 &requested,
922 ));
923 fallbacks.push(harmonize_descriptor(
924 FontDescriptor::new(StandardFont::TimesRoman.family_name()),
925 &requested,
926 ));
927 fallbacks.push(harmonize_descriptor(
928 FontDescriptor::new(StandardFont::Courier.family_name()),
929 &requested,
930 ));
931 }
932 }
933
934 for fallback in fallbacks {
935 if let Ok(registered) = font_store.get_font(&fallback) {
936 return Ok((
937 registered.descriptor().clone(),
938 Some(registered.source().clone()),
939 ));
940 }
941 }
942
943 Err(Error::UnresolvedFont {
944 family: requested.family().to_string(),
945 })
946 }
947
948 fn itemize_run(
949 &self,
950 range: TextRange,
951 text: &str,
952 attributes: &TextAttributes,
953 ) -> Vec<ItemizedRun> {
954 if text.is_empty() {
955 return Vec::new();
956 }
957
958 let explicit_direction = attributes.direction();
959 let mut items = Vec::new();
960 let mut chunk_start = 0;
961 let mut current_script = Script::Common;
962 let mut current_direction =
963 explicit_direction.unwrap_or_else(|| resolve_direction(None, self.config.bidi, text));
964
965 for (offset, character) in text.char_indices() {
966 let script = detect_script(character);
967 let direction = explicit_direction.unwrap_or_else(|| {
968 direction_for_char(character)
969 .unwrap_or_else(|| resolve_direction(None, self.config.bidi, text))
970 });
971
972 let script_changed = current_script != Script::Common
973 && script != Script::Common
974 && script != current_script;
975 let direction_changed = direction != current_direction
976 && direction_for_char(character).is_some()
977 && has_strong_direction(&text[chunk_start..offset]);
978
979 if offset > chunk_start && (script_changed || direction_changed) {
980 let item_text = text[chunk_start..offset].to_string();
981 items.push(ItemizedRun {
982 range: TextRange::new(range.start() + chunk_start, range.start() + offset),
983 text: item_text,
984 });
985 chunk_start = offset;
986 }
987
988 if script != Script::Common {
989 current_script = script;
990 }
991 current_direction = direction;
992 }
993
994 items.push(ItemizedRun {
995 range: TextRange::new(range.start() + chunk_start, range.end()),
996 text: text[chunk_start..].to_string(),
997 });
998
999 items
1000 }
1001
1002 fn layout_runs(
1003 &self,
1004 runs: Vec<TextRun>,
1005 container: TextContainer,
1006 font_store: Option<&FontStore>,
1007 ) -> Result<TextLayout> {
1008 let mut lines = Vec::new();
1009 let mut pending = Vec::<PendingFragment>::new();
1010 let mut current_width = 0.0_f32;
1011 let mut current_y = container.rect().y.value();
1012 let bottom = container.rect().bottom().value();
1013 let mut overflowed = false;
1014
1015 'layout: for run in &runs {
1016 let tokens = tokenize_run(run, self.config.line_breaking);
1017 for token in tokens {
1018 if token.is_newline {
1019 if !pending.is_empty() {
1020 let line = finalize_line(
1021 &pending,
1022 container.rect(),
1023 current_y,
1024 self.config.justification,
1025 false,
1026 );
1027 if line.rect().bottom().value() > bottom {
1028 overflowed = true;
1029 break 'layout;
1030 }
1031 current_y = line.rect().bottom().value();
1032 lines.push(line);
1033 pending.clear();
1034 current_width = 0.0;
1035 }
1036 continue;
1037 }
1038
1039 let available_width = container.rect().width.value() - current_width;
1040 if !pending.is_empty() && token.width.value() > available_width {
1041 let line = finalize_line(
1042 &pending,
1043 container.rect(),
1044 current_y,
1045 self.config.justification,
1046 false,
1047 );
1048 if line.rect().bottom().value() > bottom {
1049 overflowed = true;
1050 break 'layout;
1051 }
1052 current_y = line.rect().bottom().value();
1053 lines.push(line);
1054 pending.clear();
1055 current_width = 0.0;
1056 }
1057
1058 let available_width = container.rect().width.value() - current_width;
1059 if token.width.value() > available_width
1060 && !token.is_whitespace
1061 && let Some((head, tail)) =
1062 self.split_token_to_fit(run, &token, available_width, font_store)
1063 {
1064 pending.push(head);
1065 let line = finalize_line(
1066 &pending,
1067 container.rect(),
1068 current_y,
1069 self.config.justification,
1070 false,
1071 );
1072 if line.rect().bottom().value() > bottom {
1073 overflowed = true;
1074 break 'layout;
1075 }
1076 current_y = line.rect().bottom().value();
1077 lines.push(line);
1078 pending.clear();
1079 current_width = 0.0;
1080
1081 if let Some(tail) = tail {
1082 let remainder = vec![tail];
1083 for fragment in remainder {
1084 if fragment.width.value() > container.rect().width.value()
1085 && !fragment.is_whitespace
1086 {
1087 pending.push(fragment);
1088 let line = finalize_line(
1089 &pending,
1090 container.rect(),
1091 current_y,
1092 self.config.justification,
1093 false,
1094 );
1095 if line.rect().bottom().value() > bottom {
1096 overflowed = true;
1097 break 'layout;
1098 }
1099 current_y = line.rect().bottom().value();
1100 lines.push(line);
1101 pending.clear();
1102 current_width = 0.0;
1103 } else {
1104 current_width += fragment.width.value();
1105 pending.push(fragment);
1106 }
1107 }
1108 }
1109 continue;
1110 }
1111
1112 current_width += token.width.value();
1113 pending.push(PendingFragment::from_token(run, token));
1114 }
1115 }
1116
1117 if !overflowed && !pending.is_empty() {
1118 let line = finalize_line(
1119 &pending,
1120 container.rect(),
1121 current_y,
1122 self.config.justification,
1123 true,
1124 );
1125 if line.rect().bottom().value() <= bottom {
1126 lines.push(line);
1127 } else {
1128 overflowed = true;
1129 }
1130 }
1131
1132 if let Some(max_lines) = container.max_lines()
1133 && lines.len() > max_lines
1134 {
1135 lines.truncate(max_lines);
1136 overflowed = true;
1137 }
1138
1139 let max_width = lines
1140 .iter()
1141 .map(|line| line.rect().width.value())
1142 .fold(0.0_f32, f32::max);
1143 let total_height = lines
1144 .last()
1145 .map(|line| line.rect().bottom().value() - container.rect().y.value())
1146 .unwrap_or(0.0);
1147
1148 Ok(TextLayout {
1149 lines,
1150 runs,
1151 bounds: TextRect::from_values(
1152 container.rect().x.value(),
1153 container.rect().y.value(),
1154 max_width,
1155 total_height,
1156 ),
1157 overflowed,
1158 })
1159 }
1160
1161 fn split_token_to_fit(
1162 &self,
1163 run: &TextRun,
1164 token: &Token,
1165 available_width: f32,
1166 font_store: Option<&FontStore>,
1167 ) -> Option<(PendingFragment, Option<PendingFragment>)> {
1168 if available_width <= 0.0 {
1169 return None;
1170 }
1171
1172 if let Some((head, tail)) =
1173 self.try_hyphenate_token(run, token, available_width, font_store)
1174 {
1175 return Some((head, Some(tail)));
1176 }
1177
1178 if matches!(
1179 self.config.line_breaking,
1180 LineBreaking::CharacterBoundary | LineBreaking::WordBoundaryOrCharacter
1181 ) {
1182 return split_token_by_characters(run, token, available_width);
1183 }
1184
1185 None
1186 }
1187
1188 fn try_hyphenate_token(
1189 &self,
1190 run: &TextRun,
1191 token: &Token,
1192 available_width: f32,
1193 font_store: Option<&FontStore>,
1194 ) -> Option<(PendingFragment, PendingFragment)> {
1195 let WordHyphenation::Enabled {
1196 min_word_chars,
1197 marker,
1198 } = self.config.word_hyphenation
1199 else {
1200 return None;
1201 };
1202
1203 let store = font_store?;
1204 if token.text.chars().count() < min_word_chars {
1205 return None;
1206 }
1207
1208 let parts = store.hyphenate(&token.text);
1209 if parts.len() <= 1 {
1210 return None;
1211 }
1212
1213 let mut byte_count = 0;
1214 let mut head_text = String::new();
1215 let mut best_split = None;
1216 for (index, part) in parts.iter().enumerate().take(parts.len().saturating_sub(1)) {
1217 head_text.push_str(part);
1218 byte_count += part.len();
1219 let candidate = format!("{head_text}{marker}");
1220 let width = measure_text(&candidate, run.font_size, run.letter_spacing);
1221 if width <= available_width {
1222 best_split = Some((index + 1, byte_count, candidate, width));
1223 } else {
1224 break;
1225 }
1226 }
1227
1228 let (split_index, byte_count, candidate, width) = best_split?;
1229 let remainder = parts[split_index..].join("");
1230 let head = PendingFragment {
1231 range: TextRange::new(token.range.start(), token.range.start() + byte_count),
1232 text: candidate,
1233 width: Pt::new(width),
1234 direction: run.direction,
1235 script: run.script,
1236 font: run.resolved_font.clone(),
1237 font_size: run.font_size,
1238 decorations: run.decorations.clone(),
1239 inserted_hyphen: true,
1240 is_whitespace: false,
1241 };
1242 let tail = PendingFragment {
1243 range: TextRange::new(token.range.start() + byte_count, token.range.end()),
1244 width: Pt::new(measure_text(&remainder, run.font_size, run.letter_spacing)),
1245 text: remainder,
1246 direction: run.direction,
1247 script: run.script,
1248 font: run.resolved_font.clone(),
1249 font_size: run.font_size,
1250 decorations: run.decorations.clone(),
1251 inserted_hyphen: false,
1252 is_whitespace: false,
1253 };
1254 Some((head, tail))
1255 }
1256}
1257
1258#[derive(Clone, Debug)]
1259struct CanonicalRun {
1260 range: TextRange,
1261 text: String,
1262 attributes: TextAttributes,
1263}
1264
1265#[derive(Clone, Debug)]
1266struct ItemizedRun {
1267 range: TextRange,
1268 text: String,
1269}
1270
1271#[derive(Clone, Debug)]
1272struct Token {
1273 range: TextRange,
1274 text: String,
1275 width: Pt,
1276 is_whitespace: bool,
1277 is_newline: bool,
1278}
1279
1280#[derive(Clone, Debug)]
1281struct PendingFragment {
1282 range: TextRange,
1283 text: String,
1284 width: Pt,
1285 direction: TextDirection,
1286 script: Script,
1287 font: FontDescriptor,
1288 font_size: Pt,
1289 decorations: Vec<TextDecoration>,
1290 inserted_hyphen: bool,
1291 is_whitespace: bool,
1292}
1293
1294impl PendingFragment {
1295 fn from_token(run: &TextRun, token: Token) -> Self {
1296 Self {
1297 range: token.range,
1298 text: token.text,
1299 width: token.width,
1300 direction: run.direction,
1301 script: run.script,
1302 font: run.resolved_font.clone(),
1303 font_size: run.font_size,
1304 decorations: run.decorations.clone(),
1305 inserted_hyphen: false,
1306 is_whitespace: token.is_whitespace,
1307 }
1308 }
1309}
1310
1311fn validate_font_size(font_size: Pt) -> Result<()> {
1312 if font_size.value() <= 0.0 {
1313 Err(Error::InvalidFontSize {
1314 size: font_size.value(),
1315 })
1316 } else {
1317 Ok(())
1318 }
1319}
1320
1321fn harmonize_descriptor(descriptor: FontDescriptor, requested: &FontDescriptor) -> FontDescriptor {
1322 descriptor
1323 .with_style(requested.font_style())
1324 .with_weight(requested.font_weight())
1325}
1326
1327fn tokenize_run(run: &TextRun, strategy: LineBreaking) -> Vec<Token> {
1328 match strategy {
1329 LineBreaking::CharacterBoundary => run
1330 .text
1331 .char_indices()
1332 .map(|(offset, character)| {
1333 let end = offset + character.len_utf8();
1334 Token {
1335 range: TextRange::new(run.range.start() + offset, run.range.start() + end),
1336 text: character.to_string(),
1337 width: Pt::new(measure_text(
1338 &character.to_string(),
1339 run.font_size,
1340 run.letter_spacing,
1341 )),
1342 is_whitespace: character.is_whitespace() && character != '\n',
1343 is_newline: character == '\n',
1344 }
1345 })
1346 .collect(),
1347 LineBreaking::WordBoundary | LineBreaking::WordBoundaryOrCharacter => {
1348 let mut tokens = Vec::new();
1349 let mut start = None;
1350 let mut current_kind = None::<TokenKind>;
1351
1352 for (offset, character) in run.text.char_indices() {
1353 let kind = if character == '\n' {
1354 TokenKind::Newline
1355 } else if character.is_whitespace() {
1356 TokenKind::Whitespace
1357 } else {
1358 TokenKind::Word
1359 };
1360
1361 if current_kind.is_none() {
1362 start = Some(offset);
1363 current_kind = Some(kind);
1364 continue;
1365 }
1366
1367 if current_kind != Some(kind) || kind == TokenKind::Newline {
1368 if let (Some(token_start), Some(token_kind)) = (start, current_kind) {
1369 push_token(&mut tokens, run, token_start, offset, token_kind);
1370 }
1371 start = Some(offset);
1372 current_kind = Some(kind);
1373 }
1374
1375 if kind == TokenKind::Newline {
1376 if let Some(token_start) = start {
1377 push_token(
1378 &mut tokens,
1379 run,
1380 token_start,
1381 offset + character.len_utf8(),
1382 TokenKind::Newline,
1383 );
1384 }
1385 start = None;
1386 current_kind = None;
1387 }
1388 }
1389
1390 if let (Some(token_start), Some(token_kind)) = (start, current_kind) {
1391 push_token(&mut tokens, run, token_start, run.text.len(), token_kind);
1392 }
1393
1394 tokens
1395 }
1396 }
1397}
1398
1399#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1400enum TokenKind {
1401 Word,
1402 Whitespace,
1403 Newline,
1404}
1405
1406fn push_token(tokens: &mut Vec<Token>, run: &TextRun, start: usize, end: usize, kind: TokenKind) {
1407 let text = run.text[start..end].to_string();
1408 tokens.push(Token {
1409 range: TextRange::new(run.range.start() + start, run.range.start() + end),
1410 width: Pt::new(measure_text(&text, run.font_size, run.letter_spacing)),
1411 is_whitespace: kind == TokenKind::Whitespace,
1412 is_newline: kind == TokenKind::Newline,
1413 text,
1414 });
1415}
1416
1417fn split_token_by_characters(
1418 run: &TextRun,
1419 token: &Token,
1420 available_width: f32,
1421) -> Option<(PendingFragment, Option<PendingFragment>)> {
1422 let mut last_fit = None;
1423
1424 for (offset, _) in token.text.char_indices().skip(1) {
1425 let head = &token.text[..offset];
1426 let width = measure_text(head, run.font_size, run.letter_spacing);
1427 match width.partial_cmp(&available_width) {
1428 Some(Ordering::Greater) => break,
1429 _ => {
1430 last_fit = Some((offset, width));
1431 }
1432 }
1433 }
1434
1435 let (offset, width) = last_fit?;
1436 let head = PendingFragment {
1437 range: TextRange::new(token.range.start(), token.range.start() + offset),
1438 text: token.text[..offset].to_string(),
1439 width: Pt::new(width),
1440 direction: run.direction,
1441 script: run.script,
1442 font: run.resolved_font.clone(),
1443 font_size: run.font_size,
1444 decorations: run.decorations.clone(),
1445 inserted_hyphen: false,
1446 is_whitespace: false,
1447 };
1448 let tail_text = token.text[offset..].to_string();
1449 let tail = if tail_text.is_empty() {
1450 None
1451 } else {
1452 Some(PendingFragment {
1453 range: TextRange::new(token.range.start() + offset, token.range.end()),
1454 width: Pt::new(measure_text(&tail_text, run.font_size, run.letter_spacing)),
1455 text: tail_text,
1456 direction: run.direction,
1457 script: run.script,
1458 font: run.resolved_font.clone(),
1459 font_size: run.font_size,
1460 decorations: run.decorations.clone(),
1461 inserted_hyphen: false,
1462 is_whitespace: false,
1463 })
1464 };
1465
1466 Some((head, tail))
1467}
1468
1469fn finalize_line(
1470 pending: &[PendingFragment],
1471 container: TextRect,
1472 line_y: f32,
1473 justification: Justification,
1474 is_last_line: bool,
1475) -> LineFragment {
1476 let natural_width = pending
1477 .iter()
1478 .map(|fragment| fragment.width.value())
1479 .sum::<f32>();
1480 let max_font_size = pending
1481 .iter()
1482 .map(|fragment| fragment.font_size.value())
1483 .fold(0.0_f32, f32::max);
1484 let line_height = if max_font_size > 0.0 {
1485 max_font_size * 1.2
1486 } else {
1487 0.0
1488 };
1489 let baseline = line_y + (max_font_size * 0.8);
1490 let direction = pending
1491 .iter()
1492 .find(|fragment| !fragment.is_whitespace)
1493 .map(|fragment| fragment.direction)
1494 .unwrap_or(TextDirection::Ltr);
1495 let available_width = container.width.value();
1496 let whitespace_slots = pending
1497 .iter()
1498 .filter(|fragment| fragment.is_whitespace)
1499 .count();
1500 let justify_fully =
1501 justification == Justification::Full && !is_last_line && whitespace_slots > 0;
1502 let extra_per_whitespace = if justify_fully && available_width > natural_width {
1503 (available_width - natural_width) / whitespace_slots as f32
1504 } else {
1505 0.0
1506 };
1507 let line_width = if justify_fully {
1508 available_width
1509 } else {
1510 natural_width.min(available_width.max(natural_width))
1511 };
1512 let slack = (available_width - natural_width).max(0.0);
1513
1514 let mut cursor = match (direction, justification) {
1515 (TextDirection::Ltr, Justification::End) => container.x.value() + slack,
1516 (TextDirection::Ltr, Justification::Center) => container.x.value() + (slack / 2.0),
1517 (TextDirection::Rtl, Justification::End) => container.right().value() - slack,
1518 (TextDirection::Rtl, Justification::Center) => container.right().value() - (slack / 2.0),
1519 (TextDirection::Rtl, _) => container.right().value(),
1520 _ => container.x.value(),
1521 };
1522
1523 let mut fragments = Vec::with_capacity(pending.len());
1524 for fragment in pending {
1525 let extra_width = if justify_fully && fragment.is_whitespace {
1526 extra_per_whitespace
1527 } else {
1528 0.0
1529 };
1530 let fragment_width = fragment.width.value() + extra_width;
1531 let x = match direction {
1532 TextDirection::Ltr => {
1533 let x = cursor;
1534 cursor += fragment_width;
1535 x
1536 }
1537 TextDirection::Rtl => {
1538 cursor -= fragment_width;
1539 cursor
1540 }
1541 };
1542
1543 fragments.push(TextFragment {
1544 range: fragment.range,
1545 text: fragment.text.clone(),
1546 rect: TextRect::from_values(x, line_y, fragment_width, line_height),
1547 baseline: Pt::new(baseline),
1548 direction: fragment.direction,
1549 script: fragment.script,
1550 font: fragment.font.clone(),
1551 font_size: fragment.font_size,
1552 decorations: fragment.decorations.clone(),
1553 inserted_hyphen: fragment.inserted_hyphen,
1554 whitespace: fragment.is_whitespace,
1555 });
1556 }
1557
1558 LineFragment {
1559 rect: TextRect::from_values(container.x.value(), line_y, line_width, line_height),
1560 baseline: Pt::new(baseline),
1561 direction,
1562 justification,
1563 fragments,
1564 }
1565}
1566
1567fn measure_text(text: &str, font_size: Pt, letter_spacing: Pt) -> f32 {
1568 let mut width = 0.0_f32;
1569 let mut characters = 0_usize;
1570
1571 for character in text.chars() {
1572 width += glyph_advance(character, font_size);
1573 characters += 1;
1574 }
1575
1576 if characters > 1 {
1577 width += letter_spacing.value() * (characters.saturating_sub(1) as f32);
1578 }
1579
1580 width
1581}
1582
1583fn glyph_advance(character: char, font_size: Pt) -> f32 {
1584 let multiplier = if character == ' ' {
1585 0.33
1586 } else if character == '\t' {
1587 1.32
1588 } else if matches!(detect_script(character), Script::Han) {
1589 1.0
1590 } else if matches!(detect_script(character), Script::Arabic | Script::Hebrew) {
1591 0.68
1592 } else if character.is_ascii_uppercase() {
1593 0.62
1594 } else if character.is_ascii_punctuation() {
1595 0.35
1596 } else if character.is_ascii_digit() {
1597 0.55
1598 } else if character.is_alphabetic() {
1599 0.56
1600 } else {
1601 0.5
1602 };
1603
1604 font_size.value() * multiplier
1605}
1606
1607fn resolve_direction(explicit: Option<TextDirection>, bidi: BidiMode, text: &str) -> TextDirection {
1608 if let Some(direction) = explicit {
1609 return direction;
1610 }
1611
1612 match bidi {
1613 BidiMode::LeftToRight => TextDirection::Ltr,
1614 BidiMode::RightToLeft => TextDirection::Rtl,
1615 BidiMode::Auto => text
1616 .chars()
1617 .find_map(direction_for_char)
1618 .unwrap_or(TextDirection::Ltr),
1619 }
1620}
1621
1622fn has_strong_direction(text: &str) -> bool {
1623 text.chars()
1624 .any(|character| direction_for_char(character).is_some())
1625}
1626
1627fn direction_for_char(character: char) -> Option<TextDirection> {
1628 match detect_script(character) {
1629 Script::Arabic | Script::Hebrew => Some(TextDirection::Rtl),
1630 Script::Latin | Script::Cyrillic | Script::Han => Some(TextDirection::Ltr),
1631 Script::Common | Script::Unknown => None,
1632 }
1633}
1634
1635fn detect_script_in_text(text: &str) -> Script {
1636 text.chars()
1637 .map(detect_script)
1638 .find(|script| *script != Script::Common)
1639 .unwrap_or(Script::Common)
1640}
1641
1642fn detect_script(character: char) -> Script {
1643 let code = character as u32;
1644
1645 if character.is_ascii_alphabetic() {
1646 Script::Latin
1647 } else if character.is_whitespace()
1648 || character.is_ascii_punctuation()
1649 || character.is_ascii_digit()
1650 {
1651 Script::Common
1652 } else if (0x0600..=0x06ff).contains(&code)
1653 || (0x0750..=0x077f).contains(&code)
1654 || (0x08a0..=0x08ff).contains(&code)
1655 {
1656 Script::Arabic
1657 } else if (0x0590..=0x05ff).contains(&code) {
1658 Script::Hebrew
1659 } else if (0x0400..=0x052f).contains(&code) {
1660 Script::Cyrillic
1661 } else if (0x4e00..=0x9fff).contains(&code)
1662 || (0x3400..=0x4dbf).contains(&code)
1663 || (0xf900..=0xfaff).contains(&code)
1664 {
1665 Script::Han
1666 } else if character.is_alphabetic() {
1667 Script::Latin
1668 } else {
1669 Script::Unknown
1670 }
1671}
1672
1673#[cfg(test)]
1674mod tests {
1675 use super::*;
1676 use graphitepdf_font::FontStore;
1677
1678 #[test]
1679 fn converts_text_block_into_attributed_string_runs() {
1680 let block = TextBlock::new([
1681 TextSpan::new("Hello")
1682 .expect("valid span")
1683 .with_font(FontDescriptor::new("Helvetica"))
1684 .with_font_size(Pt::new(14.0))
1685 .expect("valid font size"),
1686 TextSpan::new(" world")
1687 .expect("valid span")
1688 .with_font_size(Pt::new(12.0))
1689 .expect("valid font size"),
1690 ]);
1691
1692 let attributed = block
1693 .to_attributed_string()
1694 .expect("block should convert to attributed text");
1695
1696 assert_eq!(attributed.text(), "Hello world");
1697 assert_eq!(attributed.runs().len(), 2);
1698 assert_eq!(attributed.runs()[0].range(), TextRange::new(0, 5));
1699 assert_eq!(
1700 attributed.runs()[0]
1701 .attributes()
1702 .font()
1703 .map(|font| font.family().to_string()),
1704 Some(String::from("Helvetica"))
1705 );
1706 }
1707
1708 #[test]
1709 fn itemizes_scripts_and_substitutes_missing_fonts() {
1710 let store = FontStore::new();
1711 let attributes = TextAttributes::default()
1712 .with_font(FontDescriptor::new("MissingFamily"))
1713 .with_font_size(Pt::new(12.0))
1714 .expect("font size is valid");
1715 let attributed = AttributedString::new("Hello مرحبا")
1716 .expect("text should be valid")
1717 .with_default_attributes(attributes)
1718 .expect("default attributes should be valid");
1719 let container = TextContainer::new(TextRect::from_values(0.0, 0.0, 200.0, 80.0))
1720 .expect("container should be valid");
1721 let engine = TextEngine::new(TextEngineConfig {
1722 font_substitution: FontSubstitution::FallbackFamilies(vec![FontDescriptor::new(
1723 "Helvetica",
1724 )]),
1725 ..TextEngineConfig::default()
1726 });
1727
1728 let layout = engine
1729 .layout(&attributed, &container, Some(&store))
1730 .expect("layout should succeed with fallback fonts");
1731
1732 assert!(layout.runs().len() >= 2);
1733 assert_eq!(layout.runs()[0].script(), Script::Latin);
1734 assert!(
1735 layout
1736 .runs()
1737 .iter()
1738 .any(|run| run.script() == Script::Arabic)
1739 );
1740 assert!(
1741 layout
1742 .runs()
1743 .iter()
1744 .all(|run| run.resolved_font().family() == "Helvetica")
1745 );
1746 }
1747
1748 #[test]
1749 fn hyphenates_long_words_in_narrow_containers() {
1750 let mut store = FontStore::new();
1751 store.register_hyphenation_callback(|word| {
1752 if word == "graphitepdf" {
1753 vec!["graph".into(), "ite".into(), "pdf".into()]
1754 } else {
1755 vec![word.to_string()]
1756 }
1757 });
1758
1759 let attributed = AttributedString::new("graphitepdf")
1760 .expect("text should be valid")
1761 .with_default_attributes(
1762 TextAttributes::default()
1763 .with_font(FontDescriptor::new("Helvetica"))
1764 .with_font_size(Pt::new(12.0))
1765 .expect("font size is valid"),
1766 )
1767 .expect("default attributes should be valid");
1768 let container = TextContainer::new(TextRect::from_values(0.0, 0.0, 40.0, 100.0))
1769 .expect("container should be valid");
1770 let engine = TextEngine::new(TextEngineConfig {
1771 word_hyphenation: WordHyphenation::Enabled {
1772 min_word_chars: 6,
1773 marker: '-',
1774 },
1775 line_breaking: LineBreaking::WordBoundaryOrCharacter,
1776 ..TextEngineConfig::default()
1777 });
1778
1779 let layout = engine
1780 .layout(&attributed, &container, Some(&store))
1781 .expect("hyphenated layout should succeed");
1782
1783 assert!(layout.lines().len() >= 2);
1784 assert_eq!(layout.lines()[0].fragments()[0].text(), "graph-");
1785 assert!(layout.lines()[0].fragments()[0].inserted_hyphen());
1786 }
1787
1788 #[test]
1789 fn fully_justifies_intermediate_lines_and_preserves_decorations() {
1790 let store = FontStore::new();
1791 let attributed = AttributedString::new("rust text layout")
1792 .expect("text should be valid")
1793 .with_default_attributes(
1794 TextAttributes::default()
1795 .with_font(FontDescriptor::new("Helvetica"))
1796 .with_font_size(Pt::new(12.0))
1797 .expect("font size is valid")
1798 .with_decoration(TextDecoration::new(TextDecorationKind::Underline)),
1799 )
1800 .expect("default attributes should be valid");
1801 let container = TextContainer::new(TextRect::from_values(0.0, 0.0, 75.0, 100.0))
1802 .expect("container should be valid");
1803 let engine = TextEngine::new(TextEngineConfig {
1804 justification: Justification::Full,
1805 ..TextEngineConfig::default()
1806 });
1807
1808 let layout = engine
1809 .layout(&attributed, &container, Some(&store))
1810 .expect("layout should succeed");
1811
1812 assert!(layout.lines().len() >= 2);
1813 assert_eq!(layout.lines()[0].rect().width.value(), 75.0);
1814 assert!(
1815 layout.lines()[0]
1816 .fragments()
1817 .iter()
1818 .all(|fragment| !fragment.decorations().is_empty())
1819 );
1820 }
1821
1822 #[test]
1823 fn rtl_layout_places_fragments_from_right_to_left() {
1824 let store = FontStore::new();
1825 let attributed = AttributedString::new("مرحبا بكم")
1826 .expect("text should be valid")
1827 .with_default_attributes(
1828 TextAttributes::default()
1829 .with_font(FontDescriptor::new("Helvetica"))
1830 .with_font_size(Pt::new(12.0))
1831 .expect("font size is valid"),
1832 )
1833 .expect("default attributes should be valid");
1834 let container = TextContainer::new(TextRect::from_values(0.0, 0.0, 120.0, 60.0))
1835 .expect("container should be valid");
1836 let engine = TextEngine::new(TextEngineConfig {
1837 bidi: BidiMode::RightToLeft,
1838 ..TextEngineConfig::default()
1839 });
1840
1841 let layout = engine
1842 .layout(&attributed, &container, Some(&store))
1843 .expect("layout should succeed");
1844 let first_line = &layout.lines()[0];
1845
1846 assert_eq!(first_line.direction(), TextDirection::Rtl);
1847 assert!(
1848 first_line.fragments()[0].rect().x.value() > first_line.fragments()[2].rect().x.value()
1849 );
1850 }
1851}