Skip to main content

layout/flow/inline/
text_run.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::mem;
6use std::ops::Range;
7use std::sync::Arc;
8
9use app_units::Au;
10use atomic_refcell::AtomicRefCell;
11use fonts::font_feature_values::ResolvedFontVariantAlternates;
12use fonts::{
13    ByteIndex, FontContext, FontRef, ShapedText, ShapedTextSlice, ShapingFlags, ShapingOptions,
14    TextByteRange,
15};
16use icu_locid::subtags::Language;
17use icu_properties::{self, LineBreak};
18use layout_api::ScriptSelection;
19use log::warn;
20use malloc_size_of_derive::MallocSizeOf;
21use servo_arc::Arc as ServoArc;
22use servo_base::text::{Utf32CodeUnits, is_bidi_control};
23use smallvec::SmallVec;
24use style::Zero;
25use style::computed_values::font_kerning::T as FontKerning;
26use style::computed_values::font_variant_position::T as FontVariantPosition;
27use style::computed_values::text_rendering::T as TextRendering;
28use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
29use style::font_face::FontLanguageOverride;
30use style::properties::ComputedValues;
31use style::values::computed::{
32    FontFeatureSettings, FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric,
33};
34use unicode_bidi::Level;
35use unicode_script::Script;
36
37use super::{InlineFormattingContextLayout, SharedInlineStyles};
38use crate::ArcRefCell;
39use crate::context::LayoutContext;
40use crate::dom::WeakLayoutBox;
41use crate::flow::inline::line::TextRunOffsets;
42use crate::flow::inline::shaping_queue::ShapingQueueEntry;
43use crate::flow::inline::{BidiLevels, LineBlockSizes, LineItem, SegmentContentFlags};
44use crate::fragment_tree::BaseFragmentInfo;
45
46// There are two reasons why we might want to break at the start:
47//
48//  1. The line breaker told us that a break was necessary between two separate
49//     instances of sending text to it.
50//  2. We are following replaced content ie `have_deferred_soft_wrap_opportunity`.
51//
52// In both cases, we don't want to do this if the first character prevents a
53// soft wrap opportunity.
54#[derive(PartialEq)]
55enum SegmentStartSoftWrapPolicy {
56    Force,
57    FollowLinebreaker,
58}
59
60/// A data structure which contains information used when shaping a [`TextRunSegment`].
61#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
62pub(crate) struct FontAndScriptInfo {
63    /// The script used when shaping a [`TextRunSegment`].
64    pub script: Script,
65    /// The rest of the font information which is never modified.
66    #[conditional_malloc_size_of]
67    pub font_info: Arc<FontInfo>,
68}
69
70impl FontAndScriptInfo {
71    /// Creates a minimal [`FontAndScriptInfo`] for a single font, with generic language settings
72    /// and the default shaping configuration. This is only used to generate placeholders for
73    /// text carets on otherwise empty lines.
74    pub(crate) fn simple_for_font(font: FontRef) -> Self {
75        Self {
76            script: Script::Common,
77            font_info: Arc::new(FontInfo::simple_for_font(font)),
78        }
79    }
80}
81
82/// A data structure which contains information used when shaping a [`TextRunSegment`].
83#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
84pub(crate) struct FontInfo {
85    /// The font used when shaping a [`TextRunSegment`].
86    pub font: FontRef,
87    /// The BiDi [`Level`] used when shaping a [`TextRunSegment`].
88    pub bidi_level: Level,
89    /// The [`Language`] used when shaping a [`TextRunSegment`].
90    pub language: Language,
91    /// Spacing to add between each letter. Corresponds to the CSS 2.1 `letter-spacing` property.
92    ///
93    /// Letter spacing is not applied to all characters. Use [Self::letter_spacing_for_character] to
94    /// determine the amount of spacing to apply.
95    pub letter_spacing: Option<Au>,
96    /// Spacing to add between each word. Corresponds to the CSS 2.1 `word-spacing` property.
97    pub word_spacing: Option<Au>,
98    /// The [`TextRendering`] value from the original style.
99    pub text_rendering: TextRendering,
100    /// The value of the `font-kerning` property from the original style.
101    pub kerning: FontKerning,
102    /// The value of the `font-variant-ligatures` property from the original style.
103    pub ligatures: FontVariantLigatures,
104    /// The value of the `font-variant-numeric` property from the original style.
105    pub numeric: FontVariantNumeric,
106    /// The value of the `font-variant-east-asian` property from the original style.
107    pub east_asian: FontVariantEastAsian,
108    /// The value of the `font-feature-settings` property from the original style.
109    pub feature_settings: FontFeatureSettings,
110    /// The value of the `font-variant-position` property from the original style.
111    pub position: FontVariantPosition,
112    /// The value of the `font-variant-alternates` property from the original style.
113    ///
114    /// Any alternate names are already resolved at this point.
115    pub alternates: ResolvedFontVariantAlternates,
116}
117
118impl FontInfo {
119    fn simple_for_font(font: FontRef) -> Self {
120        Self {
121            font,
122            bidi_level: Level::ltr(),
123            language: Language::UND,
124            letter_spacing: None,
125            word_spacing: None,
126            text_rendering: TextRendering::Auto,
127            kerning: FontKerning::Auto,
128            ligatures: FontVariantLigatures::NORMAL,
129            numeric: FontVariantNumeric::NORMAL,
130            east_asian: FontVariantEastAsian::NORMAL,
131            feature_settings: FontFeatureSettings::normal(),
132            position: FontVariantPosition::Normal,
133            alternates: Default::default(),
134        }
135    }
136}
137
138impl From<&FontAndScriptInfo> for ShapingOptions {
139    fn from(info: &FontAndScriptInfo) -> Self {
140        let mut ligatures = info.font_info.ligatures;
141        let mut flags = ShapingFlags::empty();
142        if info.font_info.bidi_level.is_rtl() {
143            flags.insert(ShapingFlags::RTL_FLAG);
144        }
145
146        // From https://www.w3.org/TR/css-text-3/#cursive-script:
147        // Cursive scripts do not admit gaps between their letters for either
148        // justification or letter-spacing.
149        let letter_spacing = info
150            .font_info
151            .letter_spacing
152            .filter(|_| !is_cursive_script(info.script));
153        if letter_spacing.is_some() {
154            ligatures = FontVariantLigatures::NONE;
155        };
156        if info.font_info.text_rendering == TextRendering::Optimizespeed {
157            ligatures = FontVariantLigatures::NONE;
158            flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
159        }
160
161        // We currently always leave kerning enabled for "font-kerning: auto".
162        if info.font_info.kerning == FontKerning::None {
163            flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG);
164        }
165
166        Self {
167            letter_spacing,
168            word_spacing: info.font_info.word_spacing,
169            script: info.script,
170            language: info.font_info.language,
171            ligatures,
172            numeric: info.font_info.numeric,
173            east_asian: info.font_info.east_asian,
174            feature_settings: info.font_info.feature_settings.clone(),
175            position: info.font_info.position,
176            flags,
177            alternates: info.font_info.alternates.clone(),
178        }
179    }
180}
181
182#[derive(Clone, Debug, MallocSizeOf)]
183pub(crate) struct TextRunSegment {
184    /// Information about the font and language used in this text run. This is produced by
185    /// segmenting the inline formatting context's text content by font, script, and bidi level.
186    pub info: FontAndScriptInfo,
187
188    /// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content.
189    pub byte_range: Range<usize>,
190
191    /// The range of characters in the parent [`super::InlineFormattingContext`]'s text content.
192    pub character_range: Range<usize>,
193
194    /// Whether or not the linebreaker said that we should allow a line break at the start of this
195    /// segment.
196    pub break_at_start: bool,
197
198    /// The shaped runs within this segment.
199    #[conditional_malloc_size_of]
200    pub runs: Vec<Arc<ShapedTextSlice>>,
201
202    /// The shaped text that was used to produce this segment. [`Self::runs`] are slices
203    /// of this shaped text.
204    #[conditional_malloc_size_of]
205    pub shaped_text: Option<Arc<ShapedText>>,
206}
207
208impl TextRunSegment {
209    fn new(
210        info: FontAndScriptInfo,
211        byte_range: Range<usize>,
212        character_range: Range<usize>,
213    ) -> Self {
214        Self {
215            info,
216            byte_range,
217            character_range,
218            runs: Vec::new(),
219            break_at_start: false,
220            shaped_text: None,
221        }
222    }
223
224    /// Returns true if the new `Font`, `Script` and BiDi `Level` are compatible with this segment
225    /// or false otherwise.
226    fn is_compatible(
227        &self,
228        new_font: &Option<FontRef>,
229        new_script: Script,
230        new_bidi_level: Level,
231    ) -> bool {
232        if self.info.font_info.bidi_level != new_bidi_level {
233            return false;
234        }
235        if new_font
236            .as_ref()
237            .is_some_and(|new_font| !Arc::ptr_eq(&self.info.font_info.font, new_font))
238        {
239            return false;
240        }
241
242        !script_is_specific(self.info.script) ||
243            !script_is_specific(new_script) ||
244            self.info.script == new_script
245    }
246
247    /// Update this segment to end at the given byte and character index. The update will only ever
248    /// make the Script specific and will not change it otherwise.
249    fn update(&mut self, next_byte_index: usize, next_character_index: usize, new_script: Script) {
250        if !script_is_specific(self.info.script) && script_is_specific(new_script) {
251            self.info = FontAndScriptInfo {
252                script: new_script,
253                font_info: self.info.font_info.clone(),
254            };
255        }
256        self.character_range.end = next_character_index;
257        self.byte_range.end = next_byte_index;
258    }
259
260    fn layout_into_line_items(
261        &self,
262        text_run: &TextRun,
263        mut soft_wrap_policy: SegmentStartSoftWrapPolicy,
264        ifc: &mut InlineFormattingContextLayout,
265    ) {
266        if self.break_at_start && soft_wrap_policy == SegmentStartSoftWrapPolicy::FollowLinebreaker
267        {
268            soft_wrap_policy = SegmentStartSoftWrapPolicy::Force;
269        }
270
271        let mut character_range_start = self.character_range.start;
272        for (run_index, run) in self.runs.iter().enumerate() {
273            let new_character_range_end = character_range_start + run.character_count();
274            let offsets = ifc
275                .ifc
276                .shared_selection
277                .clone()
278                .or_else(|| {
279                    if text_run.document_selection.is_empty() {
280                        None
281                    } else {
282                        Some(Arc::new(AtomicRefCell::new(ScriptSelection {
283                            range: TextByteRange::new(ByteIndex::zero(), ByteIndex::zero()),
284                            character_range: text_run.document_selection.start.0..
285                                text_run.document_selection.end.0,
286                            enabled: true,
287                        })))
288                    }
289                })
290                .map(|shared_selection| TextRunOffsets {
291                    shared_selection,
292                    character_range: character_range_start..new_character_range_end,
293                });
294
295            // Break before each unbreakable run in this TextRun, except the first unless the
296            // linebreaker was set to break before the first run.
297            if run_index != 0 || soft_wrap_policy == SegmentStartSoftWrapPolicy::Force {
298                ifc.process_soft_wrap_opportunity();
299            }
300
301            ifc.push_glyph_store_to_unbreakable_segment(run.clone(), text_run, &self.info, offsets);
302            character_range_start = new_character_range_end;
303        }
304    }
305
306    pub(crate) fn is_compatible_with_old_shaping_result(&self, old_segment: &Self) -> bool {
307        old_segment.info == self.info && self.byte_range == old_segment.byte_range
308    }
309}
310
311/// A single item in a [`TextRun`].
312#[derive(Debug, MallocSizeOf)]
313pub(crate) enum TextRunItem {
314    /// A hard line break i.e. a "\n" as other types line breaks are normalized to "\n".
315    LineBreak { character_index: usize },
316    /// A preserved tab character that should advance the line to a tab stop.
317    Tab { bidi_level: Level },
318    /// Any other text for which a font can be matched. We store a `Box` here as [`TextRunSegment`]
319    /// is quite a bit larger than the other enum variants.
320    TextSegment(Box<TextRunSegment>),
321}
322
323/// A single [`TextRun`] for the box tree. These are all descendants of
324/// [`super::InlineBox`] or the root of the [`super::InlineFormattingContext`].  During
325/// box tree construction, text is split into [`TextRun`]s based on their font, script,
326/// etc. When these are created text is already shaped.
327///
328/// <https://www.w3.org/TR/css-display-3/#css-text-run>
329#[derive(Debug, MallocSizeOf)]
330pub(crate) struct TextRun {
331    /// The [`BaseFragmentInfo`] for this [`TextRun`]. Usually this comes from the
332    /// original text node in the DOM for the text.
333    pub base_fragment_info: BaseFragmentInfo,
334
335    /// A weak reference to the parent of this layout box. This becomes valid as soon
336    /// as the *parent* of this box is added to the tree.
337    pub parent_box: Option<WeakLayoutBox>,
338
339    /// The [`crate::SharedStyle`] from this [`TextRun`]s parent element. This is
340    /// shared so that incremental layout can simply update the parent element and
341    /// this [`TextRun`] will be updated automatically.
342    pub inline_styles: SharedInlineStyles,
343
344    /// The range of text in [`super::InlineFormattingContext::text_content`] of the
345    /// [`super::InlineFormattingContext`] that owns this [`TextRun`]. These are UTF-8 offsets.
346    pub text_range: Range<usize>,
347
348    /// The range of characters in this text in [`super::InlineFormattingContext::text_content`]
349    /// of the [`super::InlineFormattingContext`] that owns this [`TextRun`].
350    /// These are counting `char`s, *not* UTF-8 offsets.
351    pub character_range: Range<usize>,
352
353    /// The range of `char` characters in this `TextRun` that overlap the Document’s selection
354    pub document_selection: Range<Utf32CodeUnits>,
355
356    /// The [`TextRunItem`]s of this text run. This is produced by segmenting the incoming text
357    /// by things such as font and script as well as separating out hard line breaks.
358    /// segments, and shaped.
359    pub items: Vec<TextRunItem>,
360}
361
362impl TextRun {
363    pub(crate) fn new(
364        base_fragment_info: BaseFragmentInfo,
365        inline_styles: SharedInlineStyles,
366        text_range: Range<usize>,
367        character_range: Range<usize>,
368        document_selection: Range<Utf32CodeUnits>,
369        old_text_run: Option<ArcRefCell<TextRun>>,
370    ) -> Self {
371        // If there was a previous box tree layout of this text run, try to preserve the old shaped text.
372        let items = old_text_run
373            .map(|old_text_run| std::mem::take(&mut old_text_run.borrow_mut().items))
374            .unwrap_or_default();
375        Self {
376            base_fragment_info,
377            parent_box: None,
378            inline_styles,
379            text_range,
380            character_range,
381            document_selection,
382            items,
383        }
384    }
385
386    pub(super) fn segment(
387        &mut self,
388        self_arc_ref_cell: ArcRefCell<TextRun>,
389        formatting_context_text: &str,
390        layout_context: &LayoutContext,
391        bidi_levels: &BidiLevels,
392    ) -> SmallVec<[ShapingQueueEntry; 1]> {
393        let parent_style = self.inline_styles.style.borrow().clone();
394        let items = self.segment_text_by_font(
395            layout_context,
396            formatting_context_text,
397            bidi_levels,
398            &parent_style,
399        );
400
401        // If a previous box tree layout seeded this [`TextRun`] with old shaping results, use those
402        // to try to prevent re-shaping.
403        let mut old_text_run_items = std::mem::replace(&mut self.items, items).into_iter();
404
405        self.items
406            .iter()
407            .enumerate()
408            .map(move |(index, text_run_item)| {
409                let old_text_run_item = old_text_run_items.next();
410                ShapingQueueEntry::new(
411                    self_arc_ref_cell.clone(),
412                    text_run_item,
413                    index,
414                    old_text_run_item,
415                )
416            })
417            .collect()
418    }
419
420    /// Take the [`TextRun`]'s text and turn it into [`TextRunSegment`]s. Each segment has a matched
421    /// font and script. Fonts may differ when glyphs are found in fallback fonts.
422    /// [`super::InlineFormattingContext`].
423    fn segment_text_by_font(
424        &mut self,
425        layout_context: &LayoutContext,
426        formatting_context_text: &str,
427        bidi_levels: &BidiLevels,
428        parent_style: &ServoArc<ComputedValues>,
429    ) -> Vec<TextRunItem> {
430        let font_style = parent_style.clone_font();
431        let language = font_style._x_lang.0.parse().unwrap_or(Language::UND);
432        let language_for_shaping = Some(font_style.font_language_override)
433            .filter(|language_override| *language_override != FontLanguageOverride::normal())
434            .and_then(|language_override| {
435                // FIXME: ICU4x limits language tags to three bytes as that is limit
436                // defined by BCP 47. But OpenType defines a couple four-letter
437                // languages, and stylo correctly stores a four-byte value for the computed
438                // value of the property.
439                //
440                // https://www.w3.org/TR/css-fonts-4/#font-language-override-string-value
441                //
442                // For now we need to truncate the language tag ):
443                Language::try_from_bytes(&language_override.0.to_be_bytes()[..3]).ok()
444            })
445            .unwrap_or(language);
446        let font_size = font_style.font_size.computed_size().into();
447        let kerning = font_style.font_kerning;
448        let ligatures = font_style.font_variant_ligatures;
449        let numeric = font_style.font_variant_numeric;
450        let east_asian = font_style.font_variant_east_asian;
451        let feature_settings = font_style.font_feature_settings.clone();
452        let position = font_style.font_variant_position;
453        let alternates = font_style.font_variant_alternates.clone();
454
455        let font_group = layout_context.font_context.font_group(font_style);
456        let inherited_text_style = parent_style.get_inherited_text();
457        let word_spacing = Some(inherited_text_style.word_spacing.to_used_value(font_size));
458        let letter_spacing = inherited_text_style
459            .letter_spacing
460            .0
461            .to_used_value(font_size);
462        let letter_spacing = if !letter_spacing.is_zero() {
463            Some(letter_spacing)
464        } else {
465            None
466        };
467        let text_rendering = inherited_text_style.text_rendering;
468
469        let mut current: Option<TextRunSegment> = None;
470        let mut results = Vec::new();
471        let finish_current_segment =
472            |current: &mut Option<TextRunSegment>, results: &mut Vec<TextRunItem>| {
473                if let Some(current) = current.take() {
474                    results.push(TextRunItem::TextSegment(Box::new(current)));
475                }
476            };
477
478        let text_run_text = &formatting_context_text[self.text_range.clone()];
479        let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars());
480        // The next bytes index of the character within the entire inline formatting context's text.
481        let mut next_byte_index = self.text_range.start;
482        for (relative_character_index, (character, next_character)) in char_iterator.enumerate() {
483            // The current character index within the entire inline formatting context's text.
484            let current_character_index = self.character_range.start + relative_character_index;
485
486            let current_byte_index = next_byte_index;
487            next_byte_index += character.len_utf8();
488
489            if character == '\n' {
490                finish_current_segment(&mut current, &mut results);
491                results.push(TextRunItem::LineBreak {
492                    character_index: current_character_index,
493                });
494                continue;
495            }
496
497            if character == '\t' {
498                finish_current_segment(&mut current, &mut results);
499                results.push(TextRunItem::Tab {
500                    bidi_level: bidi_levels.level(current_byte_index),
501                });
502                continue;
503            }
504
505            let (font, script, bidi_level) = if character_cannot_change_font(character) {
506                (None, Script::Common, bidi_levels.level(current_byte_index))
507            } else {
508                (
509                    font_group.find_by_codepoint(
510                        &layout_context.font_context,
511                        character,
512                        next_character,
513                        language,
514                    ),
515                    Script::from(character),
516                    bidi_levels.level(current_byte_index),
517                )
518            };
519
520            // If the existing segment is compatible with the character, just merge the character into it.
521            if let Some(current) = current.as_mut() &&
522                current.is_compatible(&font, script, bidi_level)
523            {
524                current.update(next_byte_index, current_character_index + 1, script);
525                continue;
526            }
527
528            let Some(font) = font.or_else(|| font_group.first(&layout_context.font_context)) else {
529                continue;
530            };
531
532            let alternates = layout_context
533                .font_context
534                .resolve_font_variant_alternate_identifiers_for(
535                    &font,
536                    &alternates,
537                    layout_context.style_context.stylist,
538                );
539            let info = FontAndScriptInfo {
540                script,
541                font_info: Arc::new(FontInfo {
542                    font,
543                    bidi_level,
544                    language: language_for_shaping,
545                    word_spacing,
546                    letter_spacing,
547                    text_rendering,
548                    kerning,
549                    ligatures,
550                    numeric,
551                    east_asian,
552                    feature_settings: feature_settings.clone(),
553                    alternates,
554                    position,
555                }),
556            };
557
558            finish_current_segment(&mut current, &mut results);
559            assert!(current.is_none());
560
561            current = Some(TextRunSegment::new(
562                info,
563                current_byte_index..next_byte_index,
564                current_character_index..current_character_index + 1,
565            ));
566        }
567
568        finish_current_segment(&mut current, &mut results);
569        results
570    }
571
572    pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextLayout) {
573        if self.text_range.is_empty() {
574            return;
575        }
576
577        // If we are following replaced content, we should have a soft wrap opportunity, unless the
578        // first character of this `TextRun` prevents that soft wrap opportunity. If we see such a
579        // character it should also override the LineBreaker's indication to break at the start.
580        let have_deferred_soft_wrap_opportunity =
581            mem::replace(&mut ifc.have_deferred_soft_wrap_opportunity, false);
582        let mut soft_wrap_policy = match have_deferred_soft_wrap_opportunity {
583            true => SegmentStartSoftWrapPolicy::Force,
584            false => SegmentStartSoftWrapPolicy::FollowLinebreaker,
585        };
586
587        for item in self.items.iter() {
588            ifc.possibly_flush_deferred_forced_line_break();
589
590            match item {
591                // If this whitespace forces a line break, queue up a hard line break the next time we
592                // see any content. We don't line break immediately, because we'd like to finish processing
593                // any ongoing inline boxes before ending the line.
594                TextRunItem::LineBreak { character_index } => {
595                    ifc.defer_forced_line_break_at_character_offset(*character_index);
596                },
597                TextRunItem::Tab { bidi_level } => self.process_preserved_tab(ifc, *bidi_level),
598                TextRunItem::TextSegment(segment) => {
599                    segment.layout_into_line_items(self, soft_wrap_policy, ifc)
600                },
601            }
602            soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker;
603        }
604    }
605
606    fn process_preserved_tab(
607        &self,
608        ifc_layout: &mut InlineFormattingContextLayout,
609        bidi_level: Level,
610    ) {
611        let advance = ifc_layout.ifc.next_tab_stop_after_inline_advance(
612            &self.inline_styles.style.borrow(),
613            ifc_layout.potential_line_size().inline,
614        );
615        if advance.is_zero() {
616            return;
617        }
618
619        ifc_layout.update_unbreakable_segment_for_new_content(
620            &LineBlockSizes::zero(),
621            advance,
622            SegmentContentFlags::empty(),
623        );
624        ifc_layout.push_line_item_to_unbreakable_segment(LineItem::Tab {
625            inline_box_identifier: ifc_layout.current_inline_box_identifier(),
626            advance,
627            bidi_level,
628        });
629
630        if ifc_layout
631            .current_inline_container_state()
632            .style
633            .get_inherited_text()
634            .white_space_collapse ==
635            WhiteSpaceCollapse::BreakSpaces
636        {
637            ifc_layout.process_soft_wrap_opportunity();
638        }
639    }
640}
641
642/// From <https://www.w3.org/TR/css-text-3/#cursive-script>:
643/// Cursive scripts do not admit gaps between their letters for either justification
644/// or letter-spacing. The following Unicode scripts are included: Arabic, Hanifi
645/// Rohingya, Mandaic, Mongolian, N’Ko, Phags Pa, Syriac
646fn is_cursive_script(script: Script) -> bool {
647    matches!(
648        script,
649        Script::Arabic |
650            Script::Hanifi_Rohingya |
651            Script::Mandaic |
652            Script::Mongolian |
653            Script::Nko |
654            Script::Phags_Pa |
655            Script::Syriac
656    )
657}
658
659/// Whether or not this character should be able to change the font during segmentation.  Certain
660/// character are not rendered at all, so it doesn't matter what font we use to render them. They
661/// should just be added to the current segment.
662fn character_cannot_change_font(character: char) -> bool {
663    if character.is_control() {
664        return true;
665    }
666    if character == '\u{00A0}' {
667        return true;
668    }
669    if is_bidi_control(character) {
670        return false;
671    }
672
673    matches!(
674        icu_properties::maps::line_break().get(character),
675        LineBreak::CombiningMark |
676            LineBreak::Glue |
677            LineBreak::ZWSpace |
678            LineBreak::WordJoiner |
679            LineBreak::ZWJ
680    )
681}
682
683pub(super) fn get_font_for_first_font_for_style(
684    style: &ComputedValues,
685    font_context: &FontContext,
686) -> Option<FontRef> {
687    let font = font_context
688        .font_group(style.clone_font())
689        .first(font_context);
690    if font.is_none() {
691        warn!("Could not find font for style: {:?}", style.clone_font());
692    }
693    font
694}
695pub(crate) struct TwoCharsAtATimeIterator<InputIterator> {
696    /// The input character iterator.
697    iterator: InputIterator,
698    /// The first character to produce in the next run of the iterator.
699    next_character: Option<char>,
700}
701
702impl<InputIterator> TwoCharsAtATimeIterator<InputIterator> {
703    fn new(iterator: InputIterator) -> Self {
704        Self {
705            iterator,
706            next_character: None,
707        }
708    }
709}
710
711impl<InputIterator> Iterator for TwoCharsAtATimeIterator<InputIterator>
712where
713    InputIterator: Iterator<Item = char>,
714{
715    type Item = (char, Option<char>);
716
717    fn next(&mut self) -> Option<Self::Item> {
718        // If the iterator isn't initialized do that now.
719        if self.next_character.is_none() {
720            self.next_character = self.iterator.next();
721        }
722        let character = self.next_character?;
723        self.next_character = self.iterator.next();
724        Some((character, self.next_character))
725    }
726}
727
728pub(crate) fn script_is_specific(script: Script) -> bool {
729    script != Script::Common && script != Script::Inherited
730}