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