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, OnceLock};
8
9use app_units::Au;
10use atomic_refcell::AtomicRefCell;
11use fonts::font_feature_values::ResolvedFontVariantAlternates;
12use fonts::{FontContext, FontRef, ShapedText, ShapedTextSlice, ShapingFlags, ShapingOptions};
13use icu_locale_core::subtags::Language;
14use icu_properties::props::{EnumeratedProperty, LineBreak};
15use log::warn;
16use malloc_size_of_derive::MallocSizeOf;
17use servo_arc::Arc as ServoArc;
18use servo_base::text::{RangeAny, Utf8CodeUnits, Utf32CodeUnits, is_bidi_control};
19use smallvec::SmallVec;
20use style::Zero;
21use style::computed_values::font_kerning::T as FontKerning;
22use style::computed_values::font_variant_position::T as FontVariantPosition;
23use style::computed_values::text_rendering::T as TextRendering;
24use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
25use style::font_face::FontLanguageOverride;
26use style::properties::ComputedValues;
27use style::values::computed::{
28    FontFeatureSettings, FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric,
29};
30use unicode_bidi::Level;
31use unicode_script::Script;
32
33use super::{InlineFormattingContextLayout, SharedInlineStyles};
34use crate::ArcRefCell;
35use crate::context::LayoutContext;
36use crate::dom::WeakLayoutBox;
37use crate::flow::inline::shaping_queue::{ShapingQueueEntry, breaks_for_break_spaces};
38use crate::flow::inline::text_transform::OffsetMap;
39use crate::flow::inline::{BidiLevels, LineBlockSizes, LineItem, SegmentContentFlags};
40use crate::fragment_tree::BaseFragmentInfo;
41use crate::style_ext::ComputedValuesExt;
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 script used when shaping a [`TextRunSegment`].
61    pub script: Script,
62    /// The rest of the font information which is never modified.
63    #[conditional_malloc_size_of]
64    pub font_info: Arc<FontInfo>,
65}
66
67impl FontAndScriptInfo {
68    /// Creates a minimal [`FontAndScriptInfo`] for a single font, with generic language settings
69    /// and the default shaping configuration. This is only used to generate placeholders for
70    /// text carets on otherwise empty lines.
71    pub(crate) fn simple_for_font(font: FontRef) -> Self {
72        Self {
73            script: Script::Common,
74            font_info: Arc::new(FontInfo::simple_for_font(font)),
75        }
76    }
77}
78
79/// A data structure which contains information used when shaping a [`TextRunSegment`].
80#[derive(Clone, Debug, MallocSizeOf, PartialEq)]
81pub(crate) struct FontInfo {
82    /// The font used when shaping a [`TextRunSegment`].
83    pub font: FontRef,
84    /// The BiDi [`Level`] used when shaping a [`TextRunSegment`].
85    pub bidi_level: Level,
86    /// The [`Language`] used when shaping a [`TextRunSegment`].
87    pub language: Language,
88    /// Spacing to add between each letter. Corresponds to the CSS 2.1 `letter-spacing` property.
89    ///
90    /// Letter spacing is not applied to all characters. Use [Self::letter_spacing_for_character] to
91    /// determine the amount of spacing to apply.
92    pub letter_spacing: Option<Au>,
93    /// Spacing to add between each word. Corresponds to the CSS 2.1 `word-spacing` property.
94    pub word_spacing: Option<Au>,
95    /// The [`TextRendering`] value from the original style.
96    pub text_rendering: TextRendering,
97    /// The value of the `font-kerning` property from the original style.
98    pub kerning: FontKerning,
99    /// The value of the `font-variant-ligatures` property from the original style.
100    pub ligatures: FontVariantLigatures,
101    /// The value of the `font-variant-numeric` property from the original style.
102    pub numeric: FontVariantNumeric,
103    /// The value of the `font-variant-east-asian` property from the original style.
104    pub east_asian: FontVariantEastAsian,
105    /// The value of the `font-feature-settings` property from the original style.
106    pub feature_settings: FontFeatureSettings,
107    /// The value of the `font-variant-position` property from the original style.
108    pub position: FontVariantPosition,
109    /// The value of the `font-variant-alternates` property from the original style.
110    ///
111    /// Any alternate names are already resolved at this point.
112    pub alternates: ResolvedFontVariantAlternates,
113}
114
115impl FontInfo {
116    fn simple_for_font(font: FontRef) -> Self {
117        Self {
118            font,
119            bidi_level: Level::ltr(),
120            language: Language::UNKNOWN,
121            letter_spacing: None,
122            word_spacing: None,
123            text_rendering: TextRendering::Auto,
124            kerning: FontKerning::Auto,
125            ligatures: FontVariantLigatures::NORMAL,
126            numeric: FontVariantNumeric::NORMAL,
127            east_asian: FontVariantEastAsian::NORMAL,
128            feature_settings: FontFeatureSettings::normal(),
129            position: FontVariantPosition::Normal,
130            alternates: Default::default(),
131        }
132    }
133}
134
135impl From<&FontAndScriptInfo> for ShapingOptions {
136    fn from(info: &FontAndScriptInfo) -> Self {
137        let mut ligatures = info.font_info.ligatures;
138        let mut flags = ShapingFlags::empty();
139        if info.font_info.bidi_level.is_rtl() {
140            flags.insert(ShapingFlags::RTL_FLAG);
141        }
142
143        // From https://www.w3.org/TR/css-text-3/#cursive-script:
144        // Cursive scripts do not admit gaps between their letters for either
145        // justification or letter-spacing.
146        let letter_spacing = info
147            .font_info
148            .letter_spacing
149            .filter(|_| !is_cursive_script(info.script));
150        if letter_spacing.is_some() {
151            ligatures = FontVariantLigatures::NONE;
152        };
153        let letter_spacing = letter_spacing.unwrap_or(Au::zero());
154        let word_spacing = info.font_info.word_spacing.unwrap_or(Au::zero());
155        if info.font_info.text_rendering == TextRendering::Optimizespeed {
156            ligatures = FontVariantLigatures::NONE;
157            flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
158        }
159
160        // We currently always leave kerning enabled for "font-kerning: auto".
161        if info.font_info.kerning == FontKerning::None {
162            flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG);
163        }
164
165        Self {
166            letter_spacing,
167            word_spacing,
168            script: info.script,
169            language: info.font_info.language,
170            ligatures,
171            numeric: info.font_info.numeric,
172            east_asian: info.font_info.east_asian,
173            feature_settings: info.font_info.feature_settings.clone(),
174            position: info.font_info.position,
175            flags,
176            alternates: info.font_info.alternates.clone(),
177        }
178    }
179}
180
181#[derive(Clone, Debug, MallocSizeOf)]
182pub(crate) struct TextRunSegment {
183    /// Information about the font and language used in this text run. This is produced by
184    /// segmenting the inline formatting context's text content by font, script, and bidi level.
185    pub info: FontAndScriptInfo,
186
187    /// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content.
188    pub byte_range: Range<Utf8CodeUnits>,
189
190    /// The range of characters in the parent [`super::InlineFormattingContext`]'s text content.
191    pub character_range: Range<Utf32CodeUnits>,
192
193    /// Whether or not the linebreaker said that we should allow a line break at the start of this
194    /// segment.
195    pub break_at_start: bool,
196
197    /// The shaped runs within this segment.
198    #[conditional_malloc_size_of]
199    pub runs: Vec<Arc<ShapedTextSlice>>,
200
201    /// The shaped text that was used to produce this segment. [`Self::runs`] are slices
202    /// of this shaped text.
203    #[conditional_malloc_size_of]
204    pub shaped_text: Option<Arc<ShapedText>>,
205}
206
207impl TextRunSegment {
208    fn new(
209        info: FontAndScriptInfo,
210        byte_range: Range<Utf8CodeUnits>,
211        character_range: Range<Utf32CodeUnits>,
212    ) -> Self {
213        Self {
214            info,
215            byte_range,
216            character_range,
217            runs: Vec::new(),
218            break_at_start: false,
219            shaped_text: None,
220        }
221    }
222
223    /// Returns true if the new `Font`, `Script` and BiDi `Level` are compatible with this segment
224    /// or false otherwise.
225    fn is_compatible(
226        &self,
227        new_font: &Option<FontRef>,
228        new_script: Script,
229        new_bidi_level: Level,
230    ) -> bool {
231        if self.info.font_info.bidi_level != new_bidi_level {
232            return false;
233        }
234        if new_font
235            .as_ref()
236            .is_some_and(|new_font| !Arc::ptr_eq(&self.info.font_info.font, new_font))
237        {
238            return false;
239        }
240
241        !script_is_specific(self.info.script) ||
242            !script_is_specific(new_script) ||
243            self.info.script == new_script
244    }
245
246    /// Update this segment to end at the given byte and character index. The update will only ever
247    /// make the Script specific and will not change it otherwise.
248    fn update(
249        &mut self,
250        next_byte_index: Utf8CodeUnits,
251        next_character_index: Utf32CodeUnits,
252        new_script: Script,
253    ) {
254        if !script_is_specific(self.info.script) && script_is_specific(new_script) {
255            self.info = FontAndScriptInfo {
256                script: new_script,
257                font_info: self.info.font_info.clone(),
258            };
259        }
260        self.character_range.end = next_character_index;
261        self.byte_range.end = next_byte_index;
262    }
263
264    fn layout_into_line_items(
265        &self,
266        text_run: &TextRun,
267        mut soft_wrap_policy: SegmentStartSoftWrapPolicy,
268        ifc: &mut InlineFormattingContextLayout,
269    ) {
270        if self.break_at_start && soft_wrap_policy == SegmentStartSoftWrapPolicy::FollowLinebreaker
271        {
272            soft_wrap_policy = SegmentStartSoftWrapPolicy::Force;
273        }
274
275        let mut character_range_start = self.character_range.start;
276        for (run_index, run) in self.runs.iter().enumerate() {
277            let new_character_range_end = character_range_start + run.character_count();
278
279            // Break before each unbreakable run in this TextRun, except the first unless the
280            // linebreaker was set to break before the first run.
281            if run_index != 0 || soft_wrap_policy == SegmentStartSoftWrapPolicy::Force {
282                ifc.process_soft_wrap_opportunity();
283            }
284
285            let run_start = text_run.run_data.character_range_in_ifc_text.start;
286            ifc.push_shaped_text_slice_to_unbreakable_segment(
287                run.clone(),
288                text_run,
289                &self.info,
290                character_range_start - run_start..new_character_range_end - run_start,
291            );
292
293            character_range_start = new_character_range_end;
294        }
295    }
296
297    pub(crate) fn is_compatible_with_old_shaping_result(&self, old_segment: &Self) -> bool {
298        old_segment.info == self.info && self.byte_range == old_segment.byte_range
299    }
300}
301
302#[derive(Clone, Debug, MallocSizeOf)]
303pub(crate) struct CaretPlaceholder {
304    /// The [`TextFragmentRunData`] of the [`TextRun`] that contains this caret placeholder.
305    #[conditional_malloc_size_of]
306    pub run_data: Arc<SharedTextRunData>,
307    /// The `BaseFragmentInfo` of the originating text node that this caret placeholder is in.
308    pub base_fragment_info: BaseFragmentInfo,
309    /// Character index of the preserved newline in the IFC's transformed text, relative
310    /// to the start of the DOM node.
311    pub character_index: Utf32CodeUnits,
312}
313
314/// A single item in a [`TextRun`].
315#[derive(Debug, MallocSizeOf)]
316pub(crate) enum TextRunItem {
317    /// A hard line break i.e. a "\n" as other types line breaks are normalized to "\n".
318    LineBreak(Option<CaretPlaceholder>),
319    /// A preserved tab character that should advance the line to a tab stop.
320    Tab {
321        bidi_level: Level,
322        break_at_start: bool,
323    },
324    /// Any other text for which a font can be matched. We store a `Box` here as [`TextRunSegment`]
325    /// is quite a bit larger than the other enum variants.
326    TextSegment(Box<TextRunSegment>),
327}
328
329/// A data structure that holds per-`TextRun` data used on `TextFragment`s.
330/// This ensures that the data is not duplicated between fragments.
331#[derive(Debug, MallocSizeOf)]
332pub(crate) struct SharedTextRunData {
333    /// The text content of the [`InlineFormattingContext`] that contains this `TextRun`.
334    #[conditional_malloc_size_of]
335    pub text_content: Arc<OnceLock<String>>,
336    /// The [`crate::SharedStyle`] from this `TextRun`'s parent element. This is
337    /// shared so that incremental layout can simply update the parent element and
338    /// this [`TextRun`] will be updated automatically.
339    pub inline_styles: SharedInlineStyles,
340    /// The range of characters in this text in `InlineFormattingContext::text_content`
341    /// of the `InlineFormattingContext` that owns this `TextRun`. These are counting
342    /// `char`s, *not* UTF-8 offsets.
343    pub character_range_in_ifc_text: Range<Utf32CodeUnits>,
344    /// The original offset of this `TextRun` in the `InlineFormattingContext`'s input
345    /// text (untransformed by white space collapse and `text-transform`).
346    pub original_offset: Utf32CodeUnits,
347    /// The selected text in this `TextRun`. This may either be document selection or form control
348    /// selection.
349    // TODO: make this more compact with a pair of `AtomicUsize`?
350    pub selection: AtomicRefCell<Option<RangeAny<Utf32CodeUnits>>>,
351    /// Whether a caret should be painted when the selection is an empty range (start == end)
352    pub paints_caret: bool,
353    /// Whether a caret placeholder should be added for empty text runs or for empty
354    /// lines after a forced line break.
355    pub needs_caret_placeholder: bool,
356    /// The [`OffsetMap`] used when creating this `TextRun`'s `InlineFormattingContext`. This
357    /// is used for mapping between DOM text offsets and layout text offsets (and vice-versa).
358    pub offset_map: ArcRefCell<OffsetMap>,
359}
360
361impl SharedTextRunData {
362    /// Map a range in the originating `TextRun`'s DOM node text into the range in the
363    /// `TextRun`'s layout transformed (by white space collapse and `text-transform`)
364    /// text.
365    pub(crate) fn map_dom_range_to_transformed_range(
366        &self,
367        dom_range: RangeAny<Utf32CodeUnits>,
368    ) -> Range<Utf32CodeUnits> {
369        let offset_map = self.offset_map.borrow();
370        let offset_in_ifc_text = self.character_range_in_ifc_text.start;
371        let start = if let Some(dom_start) = dom_range.start() {
372            offset_map.map(dom_start + self.original_offset) - offset_in_ifc_text
373        } else {
374            Utf32CodeUnits(0)
375        };
376        let end = if let Some(dom_end) = dom_range.end() {
377            offset_map.map(dom_end + self.original_offset) - offset_in_ifc_text
378        } else {
379            self.character_range_in_ifc_text.end - self.character_range_in_ifc_text.start
380        };
381        start..end
382    }
383
384    /// Map an offset in the originating `TextRun`s DOM node's transformed text (by white
385    /// space collapse and `text-transform`) to untransformed text for use by the DOM.
386    pub(crate) fn map_transformed_offset_to_dom_offset(
387        &self,
388        offset: Utf32CodeUnits,
389    ) -> Utf32CodeUnits {
390        self.map_transformed_ifc_offset_to_dom_offset(
391            offset + self.character_range_in_ifc_text.start,
392        )
393    }
394
395    /// Map a UTF-32 offset in the transformed `InlineFormattingContext::text_content`
396    /// into an offset in the `TextRun`'s DOM node. The input value will be clamped to
397    /// this `TextRun`'s actual text range before mapping.
398    pub(crate) fn map_transformed_ifc_offset_to_dom_offset(
399        &self,
400        offset: Utf32CodeUnits,
401    ) -> Utf32CodeUnits {
402        let run_range = &self.character_range_in_ifc_text;
403        let offset = offset.clamp(run_range.start, run_range.end);
404        self.offset_map.borrow().reverse_map(offset) - self.original_offset
405    }
406
407    /// Map an offset in the `TextRun`'s DOM node into a UTF-32 offset in the transformed
408    /// `InlineFormattingContext::text_content`.
409    pub(crate) fn map_dom_offset_to_transformed_ifc_offset(
410        &self,
411        offset: Utf32CodeUnits,
412    ) -> Utf32CodeUnits {
413        self.offset_map.borrow().map(offset + self.original_offset)
414    }
415}
416
417/// A single [`TextRun`] for the box tree. These are all descendants of
418/// [`super::InlineBox`] or the root of the [`super::InlineFormattingContext`].  During
419/// box tree construction, text is split into [`TextRun`]s based on their font, script,
420/// etc. When these are created text is already shaped.
421///
422/// <https://www.w3.org/TR/css-display-3/#css-text-run>
423#[derive(Debug, MallocSizeOf)]
424pub(crate) struct TextRun {
425    /// The [`BaseFragmentInfo`] for this [`TextRun`]. Usually this comes from the
426    /// original text node in the DOM for the text.
427    pub base_fragment_info: BaseFragmentInfo,
428
429    /// Data to be used by all [`TextFragment`]s spawned by this [`TextRun`] to avoid
430    /// having to clone the data into each fragment.
431    #[conditional_malloc_size_of]
432    pub run_data: Arc<SharedTextRunData>,
433
434    /// A weak reference to the parent of this layout box. This becomes valid as soon
435    /// as the *parent* of this box is added to the tree.
436    pub parent_box: Option<WeakLayoutBox>,
437
438    /// The range of text in [`super::InlineFormattingContext::text_content`] of the
439    /// [`super::InlineFormattingContext`] that owns this [`TextRun`]. These are UTF-8 offsets.
440    pub text_range: Range<Utf8CodeUnits>,
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        run_data: Arc<SharedTextRunData>,
452        text_range: Range<Utf8CodeUnits>,
453        old_text_run: Option<ArcRefCell<TextRun>>,
454    ) -> Self {
455        // If there was a previous box tree layout of this text run, try to preserve the old shaped text.
456        let items = old_text_run
457            .map(|old_text_run| std::mem::take(&mut old_text_run.borrow_mut().items))
458            .unwrap_or_default();
459        Self {
460            base_fragment_info,
461            run_data,
462            parent_box: None,
463            text_range,
464            items,
465        }
466    }
467
468    pub(super) fn inline_styles(&self) -> &SharedInlineStyles {
469        &self.run_data.inline_styles
470    }
471
472    pub(super) fn segment(
473        &mut self,
474        self_arc_ref_cell: ArcRefCell<TextRun>,
475        formatting_context_text: &str,
476        layout_context: &LayoutContext,
477        bidi_levels: &BidiLevels,
478    ) -> SmallVec<[ShapingQueueEntry; 1]> {
479        let parent_style = self.inline_styles().style.borrow().clone();
480        let items = self.segment_text_by_font(
481            layout_context,
482            formatting_context_text,
483            bidi_levels,
484            &parent_style,
485        );
486
487        // If a previous box tree layout seeded this [`TextRun`] with old shaping results, use those
488        // to try to prevent re-shaping.
489        let mut old_text_run_items = std::mem::replace(&mut self.items, items).into_iter();
490
491        self.items
492            .iter()
493            .enumerate()
494            .map(move |(index, text_run_item)| {
495                let old_text_run_item = old_text_run_items.next();
496                ShapingQueueEntry::new(
497                    self_arc_ref_cell.clone(),
498                    text_run_item,
499                    index,
500                    old_text_run_item,
501                )
502            })
503            .collect()
504    }
505
506    /// Take the [`TextRun`]'s text and turn it into [`TextRunSegment`]s. Each segment has a matched
507    /// font and script. Fonts may differ when glyphs are found in fallback fonts.
508    /// [`super::InlineFormattingContext`].
509    fn segment_text_by_font(
510        &mut self,
511        layout_context: &LayoutContext,
512        formatting_context_text: &str,
513        bidi_levels: &BidiLevels,
514        parent_style: &ServoArc<ComputedValues>,
515    ) -> Vec<TextRunItem> {
516        let font_style = parent_style.clone_font();
517        let language = font_style._x_lang.0.parse().unwrap_or(Language::UNKNOWN);
518        let language_for_shaping = Some(font_style.font_language_override)
519            .filter(|language_override| *language_override != FontLanguageOverride::normal())
520            .and_then(|language_override| {
521                // FIXME: ICU4x limits language tags to three bytes as that is limit
522                // defined by BCP 47. But OpenType defines a couple four-letter
523                // languages, and stylo correctly stores a four-byte value for the computed
524                // value of the property.
525                //
526                // https://www.w3.org/TR/css-fonts-4/#font-language-override-string-value
527                //
528                // For now we need to truncate the language tag ):
529                Language::try_from_utf8(&language_override.0.to_be_bytes()[..3]).ok()
530            })
531            .unwrap_or(language);
532        let font_size = font_style.font_size.computed_size().into();
533        let kerning = font_style.font_kerning;
534        let ligatures = font_style.font_variant_ligatures;
535        let numeric = font_style.font_variant_numeric;
536        let east_asian = font_style.font_variant_east_asian;
537        let feature_settings = font_style.font_feature_settings.clone();
538        let position = font_style.font_variant_position;
539        let alternates = font_style.font_variant_alternates.clone();
540
541        let font_group = layout_context.font_context.font_group(font_style);
542        let inherited_text_style = parent_style.get_inherited_text();
543        let word_spacing = Some(inherited_text_style.word_spacing.to_used_value(font_size));
544        let letter_spacing = inherited_text_style
545            .letter_spacing
546            .0
547            .to_used_value(font_size);
548        let letter_spacing = if !letter_spacing.is_zero() {
549            Some(letter_spacing)
550        } else {
551            None
552        };
553        let text_rendering = inherited_text_style.text_rendering;
554
555        let mut current: Option<TextRunSegment> = None;
556        let mut results = Vec::new();
557        let finish_current_segment =
558            |current: &mut Option<TextRunSegment>, results: &mut Vec<TextRunItem>| {
559                if let Some(current) = current.take() {
560                    results.push(TextRunItem::TextSegment(Box::new(current)));
561                }
562            };
563
564        let text_run_text =
565            &formatting_context_text[Utf8CodeUnits::to_usize_range(&self.text_range)];
566        let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars());
567
568        // The next bytes index of the character within the entire inline formatting context's text.
569        let mut next_byte_index = self.text_range.start;
570        for (relative_character_index, (character, next_character)) in char_iterator.enumerate() {
571            // The current character index within the entire inline formatting context's text.
572            let current_character_index = self.run_data.character_range_in_ifc_text.start +
573                Utf32CodeUnits(relative_character_index as u32);
574
575            let current_byte_index = next_byte_index;
576            next_byte_index += Utf8CodeUnits::length_of_char(character);
577
578            if character == '\n' {
579                finish_current_segment(&mut current, &mut results);
580                let needs_caret_placeholder = self.run_data.needs_caret_placeholder;
581                results.push(TextRunItem::LineBreak(needs_caret_placeholder.then(|| {
582                    CaretPlaceholder {
583                        run_data: self.run_data.clone(),
584                        base_fragment_info: self.base_fragment_info,
585                        // The placeholder that is placed after a newline is for the index after that newline.
586                        // The newline itself is at the end of the previous line.
587                        character_index: Utf32CodeUnits((relative_character_index + 1) as u32),
588                    }
589                })));
590                continue;
591            }
592
593            if character == '\t' {
594                finish_current_segment(&mut current, &mut results);
595
596                // There is a soft wrap opportunity between a preceding space and a tab
597                // under `break-spaces`.
598                let break_at_start = *parent_style.get_white_space_collapse() ==
599                    WhiteSpaceCollapse::BreakSpaces &&
600                    formatting_context_text[0..current_byte_index.0 as usize]
601                        .chars()
602                        .next_back()
603                        .is_some_and(breaks_for_break_spaces);
604
605                results.push(TextRunItem::Tab {
606                    bidi_level: bidi_levels.level(current_byte_index.into()),
607                    break_at_start,
608                });
609                continue;
610            }
611
612            let (font, script, bidi_level) = if character_cannot_change_font(character) {
613                (
614                    None,
615                    Script::Common,
616                    bidi_levels.level(current_byte_index.into()),
617                )
618            } else {
619                (
620                    font_group.find_by_codepoint(
621                        &layout_context.font_context,
622                        character,
623                        next_character,
624                        language,
625                    ),
626                    Script::from(character),
627                    bidi_levels.level(current_byte_index.into()),
628                )
629            };
630
631            // If the existing segment is compatible with the character, just merge the character into it.
632            if let Some(current) = current.as_mut() &&
633                current.is_compatible(&font, script, bidi_level)
634            {
635                current.update(
636                    next_byte_index,
637                    current_character_index + Utf32CodeUnits(1),
638                    script,
639                );
640                continue;
641            }
642
643            let Some(font) = font.or_else(|| font_group.first(&layout_context.font_context)) else {
644                continue;
645            };
646
647            let alternates = layout_context
648                .font_context
649                .resolve_font_variant_alternate_identifiers_for(
650                    &font,
651                    &alternates,
652                    layout_context.style_context.stylist,
653                );
654            let info = FontAndScriptInfo {
655                script,
656                font_info: Arc::new(FontInfo {
657                    font,
658                    bidi_level,
659                    language: language_for_shaping,
660                    word_spacing,
661                    letter_spacing,
662                    text_rendering,
663                    kerning,
664                    ligatures,
665                    numeric,
666                    east_asian,
667                    feature_settings: feature_settings.clone(),
668                    alternates,
669                    position,
670                }),
671            };
672
673            finish_current_segment(&mut current, &mut results);
674            assert!(current.is_none());
675
676            current = Some(TextRunSegment::new(
677                info,
678                current_byte_index..next_byte_index,
679                current_character_index..current_character_index + Utf32CodeUnits(1),
680            ));
681        }
682
683        finish_current_segment(&mut current, &mut results);
684        results
685    }
686
687    pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextLayout) {
688        if self.text_range.is_empty() {
689            if self.run_data.needs_caret_placeholder {
690                ifc.current_line.caret_placeholder = Some(CaretPlaceholder {
691                    run_data: self.run_data.clone(),
692                    base_fragment_info: self.base_fragment_info,
693                    character_index: Utf32CodeUnits(0),
694                });
695            }
696            return;
697        }
698
699        // If we are following replaced content, we should have a soft wrap opportunity, unless the
700        // first character of this `TextRun` prevents that soft wrap opportunity. If we see such a
701        // character it should also override the LineBreaker's indication to break at the start.
702        let have_deferred_soft_wrap_opportunity =
703            mem::replace(&mut ifc.have_deferred_soft_wrap_opportunity, false);
704        let mut soft_wrap_policy = match have_deferred_soft_wrap_opportunity {
705            true => SegmentStartSoftWrapPolicy::Force,
706            false => SegmentStartSoftWrapPolicy::FollowLinebreaker,
707        };
708
709        for item in self.items.iter() {
710            ifc.possibly_flush_deferred_forced_line_break();
711
712            match item {
713                // If this whitespace forces a line break, queue up a hard line break the next time we
714                // see any content. We don't line break immediately, because we'd like to finish processing
715                // any ongoing inline boxes before ending the line.
716                TextRunItem::LineBreak(caret_placeholder) => {
717                    ifc.defer_forced_line_break_at_character_offset(caret_placeholder);
718                },
719                TextRunItem::Tab {
720                    bidi_level,
721                    break_at_start,
722                } => self.process_preserved_tab(ifc, *bidi_level, *break_at_start),
723                TextRunItem::TextSegment(segment) => {
724                    segment.layout_into_line_items(self, soft_wrap_policy, ifc)
725                },
726            }
727            soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker;
728        }
729    }
730
731    fn process_preserved_tab(
732        &self,
733        ifc_layout: &mut InlineFormattingContextLayout,
734        bidi_level: Level,
735        break_at_start: bool,
736    ) {
737        if break_at_start {
738            ifc_layout.process_soft_wrap_opportunity();
739        }
740
741        let position_after_current_segment =
742            ifc_layout.current_line.inline_position + ifc_layout.current_line_segment.inline_size;
743        let style = self.inline_styles().style.borrow();
744        let advance = ifc_layout
745            .ifc
746            .next_tab_stop_after_inline_advance(&style, position_after_current_segment);
747        if advance.is_zero() {
748            return;
749        }
750
751        let conditionally_hangs = style.white_space_may_hang_conditionally();
752        let (flags, hanging_advance) = if conditionally_hangs {
753            (SegmentContentFlags::Contentful, advance)
754        } else {
755            (
756                SegmentContentFlags::Contentful |
757                    SegmentContentFlags::IncorporateTrailingWhiteSpace,
758                Au::zero(),
759            )
760        };
761
762        ifc_layout.update_unbreakable_segment_for_new_content(
763            &LineBlockSizes::zero(),
764            advance,
765            hanging_advance,
766            Au::zero(),
767            flags,
768        );
769        ifc_layout.push_line_item_to_unbreakable_segment(LineItem::Tab {
770            inline_box_identifier: ifc_layout.current_inline_box_identifier(),
771            advance,
772            bidi_level,
773            conditionally_hangs,
774        });
775
776        if *ifc_layout
777            .current_inline_container_state()
778            .style
779            .get_white_space_collapse() ==
780            WhiteSpaceCollapse::BreakSpaces
781        {
782            ifc_layout.process_soft_wrap_opportunity();
783        }
784    }
785}
786
787/// From <https://www.w3.org/TR/css-text-3/#cursive-script>:
788/// Cursive scripts do not admit gaps between their letters for either justification
789/// or letter-spacing. The following Unicode scripts are included: Arabic, Hanifi
790/// Rohingya, Mandaic, Mongolian, N’Ko, Phags Pa, Syriac
791fn is_cursive_script(script: Script) -> bool {
792    matches!(
793        script,
794        Script::Arabic |
795            Script::Hanifi_Rohingya |
796            Script::Mandaic |
797            Script::Mongolian |
798            Script::Nko |
799            Script::Phags_Pa |
800            Script::Syriac
801    )
802}
803
804/// Whether or not this character should be able to change the font during segmentation.  Certain
805/// character are not rendered at all, so it doesn't matter what font we use to render them. They
806/// should just be added to the current segment.
807fn character_cannot_change_font(character: char) -> bool {
808    if character.is_control() {
809        return true;
810    }
811    if character == '\u{00A0}' {
812        return true;
813    }
814    if is_bidi_control(character) {
815        return false;
816    }
817
818    matches!(
819        LineBreak::for_char(character),
820        LineBreak::CombiningMark |
821            LineBreak::Glue |
822            LineBreak::ZWSpace |
823            LineBreak::WordJoiner |
824            LineBreak::ZWJ
825    )
826}
827
828pub(super) fn get_font_for_first_font_for_style(
829    style: &ComputedValues,
830    font_context: &FontContext,
831) -> Option<FontRef> {
832    let font = font_context
833        .font_group(style.clone_font())
834        .first(font_context);
835    if font.is_none() {
836        warn!("Could not find font for style: {:?}", style.clone_font());
837    }
838    font
839}
840pub(crate) struct TwoCharsAtATimeIterator<InputIterator> {
841    /// The input character iterator.
842    iterator: InputIterator,
843    /// The first character to produce in the next run of the iterator.
844    next_character: Option<char>,
845}
846
847impl<InputIterator> TwoCharsAtATimeIterator<InputIterator> {
848    fn new(iterator: InputIterator) -> Self {
849        Self {
850            iterator,
851            next_character: None,
852        }
853    }
854}
855
856impl<InputIterator> Iterator for TwoCharsAtATimeIterator<InputIterator>
857where
858    InputIterator: Iterator<Item = char>,
859{
860    type Item = (char, Option<char>);
861
862    fn next(&mut self) -> Option<Self::Item> {
863        // If the iterator isn't initialized do that now.
864        if self.next_character.is_none() {
865            self.next_character = self.iterator.next();
866        }
867        let character = self.next_character?;
868        self.next_character = self.iterator.next();
869        Some((character, self.next_character))
870    }
871}
872
873pub(crate) fn script_is_specific(script: Script) -> bool {
874    script != Script::Common && script != Script::Inherited
875}