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