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, GlyphStore, LAST_RESORT_GLYPH_ADVANCE, ShapingFlags, ShapingOptions,
12};
13use icu_locid::subtags::Language;
14use log::warn;
15use malloc_size_of_derive::MallocSizeOf;
16use servo_arc::Arc as ServoArc;
17use servo_base::text::is_bidi_control;
18use style::computed_values::text_rendering::T as TextRendering;
19use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
20use style::computed_values::word_break::T as WordBreak;
21use style::properties::ComputedValues;
22use style::str::char_is_whitespace;
23use style::values::computed::OverflowWrap;
24use unicode_bidi::{BidiInfo, Level};
25use unicode_script::Script;
26use xi_unicode::linebreak_property;
27
28use super::line_breaker::LineBreaker;
29use super::{InlineFormattingContextLayout, SharedInlineStyles};
30use crate::context::LayoutContext;
31use crate::dom::WeakLayoutBox;
32use crate::flow::inline::line::TextRunOffsets;
33use crate::fragment_tree::BaseFragmentInfo;
34
35// These constants are the xi-unicode line breaking classes that are defined in
36// `table.rs`. Unfortunately, they are only identified by number.
37pub(crate) const XI_LINE_BREAKING_CLASS_CM: u8 = 9;
38pub(crate) const XI_LINE_BREAKING_CLASS_GL: u8 = 12;
39pub(crate) const XI_LINE_BREAKING_CLASS_ZW: u8 = 28;
40pub(crate) const XI_LINE_BREAKING_CLASS_WJ: u8 = 30;
41pub(crate) const XI_LINE_BREAKING_CLASS_ZWJ: u8 = 42;
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 font and language information about a run of text or
58/// glyphs processed during inline layout.
59#[derive(Clone, Debug, MallocSizeOf)]
60pub(crate) struct FontAndScriptInfo {
61    pub font: FontRef,
62    pub script: Script,
63    pub bidi_level: Level,
64}
65
66#[derive(Debug, MallocSizeOf)]
67pub(crate) struct TextRunSegment {
68    /// Information about the font and language used in this text run. This is produced by
69    /// segmenting the inline formatting context's text content by font, script, and bidi level.
70    #[conditional_malloc_size_of]
71    pub info: Arc<FontAndScriptInfo>,
72
73    /// The range of bytes in the parent [`super::InlineFormattingContext`]'s text content.
74    pub range: Range<usize>,
75
76    /// The range of characters in the parent [`super::InlineFormattingContext`]'s text content.
77    pub character_range: Range<usize>,
78
79    /// Whether or not the linebreaker said that we should allow a line break at the start of this
80    /// segment.
81    pub break_at_start: bool,
82
83    /// The shaped runs within this segment.
84    #[conditional_malloc_size_of]
85    pub runs: Vec<Arc<GlyphStore>>,
86}
87
88impl TextRunSegment {
89    fn new(
90        info: Arc<FontAndScriptInfo>,
91        start_offset: usize,
92        start_character_offset: usize,
93    ) -> Self {
94        Self {
95            info,
96            range: start_offset..start_offset,
97            character_range: start_character_offset..start_character_offset,
98            runs: Vec::new(),
99            break_at_start: false,
100        }
101    }
102
103    /// Update this segment if the Font and Script are compatible. The update will only
104    /// ever make the Script specific. Returns true if the new Font and Script are
105    /// compatible with this segment or false otherwise.
106    fn update_if_compatible(&mut self, info: &FontAndScriptInfo) -> bool {
107        if self.info.bidi_level != info.bidi_level || !Arc::ptr_eq(&self.info.font, &info.font) {
108            return false;
109        }
110
111        fn is_specific(script: Script) -> bool {
112            script != Script::Common && script != Script::Inherited
113        }
114        if !is_specific(self.info.script) && is_specific(info.script) {
115            self.info = Arc::new(info.clone());
116        }
117        info.script == self.info.script || !is_specific(info.script)
118    }
119
120    fn layout_into_line_items(
121        &self,
122        text_run: &TextRun,
123        mut soft_wrap_policy: SegmentStartSoftWrapPolicy,
124        ifc: &mut InlineFormattingContextLayout,
125    ) {
126        if self.break_at_start && soft_wrap_policy == SegmentStartSoftWrapPolicy::FollowLinebreaker
127        {
128            soft_wrap_policy = SegmentStartSoftWrapPolicy::Force;
129        }
130
131        let mut character_range_start = self.character_range.start;
132        for (run_index, run) in self.runs.iter().enumerate() {
133            ifc.possibly_flush_deferred_forced_line_break();
134
135            let new_character_range_end = character_range_start + run.character_count();
136            let offsets = ifc
137                .ifc
138                .shared_selection
139                .clone()
140                .map(|shared_selection| TextRunOffsets {
141                    shared_selection,
142                    character_range: character_range_start..new_character_range_end,
143                });
144
145            // If this whitespace forces a line break, queue up a hard line break the next time we
146            // see any content. We don't line break immediately, because we'd like to finish processing
147            // any ongoing inline boxes before ending the line.
148            if run.is_single_preserved_newline() {
149                ifc.possibly_push_empty_text_run_to_unbreakable_segment(
150                    text_run, &self.info, offsets,
151                );
152                character_range_start = new_character_range_end;
153                ifc.defer_forced_line_break();
154                continue;
155            }
156
157            // Break before each unbreakable run in this TextRun, except the first unless the
158            // linebreaker was set to break before the first run.
159            if run_index != 0 || soft_wrap_policy == SegmentStartSoftWrapPolicy::Force {
160                ifc.process_soft_wrap_opportunity();
161            }
162
163            ifc.push_glyph_store_to_unbreakable_segment(run.clone(), text_run, &self.info, offsets);
164            character_range_start = new_character_range_end;
165        }
166    }
167
168    fn shape_and_push_range(
169        &mut self,
170        range: &Range<usize>,
171        formatting_context_text: &str,
172        options: &ShapingOptions,
173    ) {
174        self.runs.push(
175            self.info
176                .font
177                .shape_text(&formatting_context_text[range.clone()], options),
178        );
179    }
180
181    /// Shape the text of this [`TextRunSegment`], first finding "words" for the shaper by processing
182    /// the linebreaks found in the owning [`super::InlineFormattingContext`]. Linebreaks are filtered,
183    /// based on the style of the parent inline box.
184    fn shape_text(
185        &mut self,
186        parent_style: &ComputedValues,
187        formatting_context_text: &str,
188        linebreaker: &mut LineBreaker,
189        shaping_options: &ShapingOptions,
190    ) {
191        // Gather the linebreaks that apply to this segment from the inline formatting context's collection
192        // of line breaks. Also add a simulated break at the end of the segment in order to ensure the final
193        // piece of text is processed.
194        let range = self.range.clone();
195        let linebreaks = linebreaker.advance_to_linebreaks_in_range(self.range.clone());
196        let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end));
197
198        self.runs.clear();
199        self.runs.reserve(linebreaks.len());
200        self.break_at_start = false;
201
202        let text_style = parent_style.get_inherited_text().clone();
203        let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
204            text_style.overflow_wrap == OverflowWrap::Anywhere ||
205            text_style.overflow_wrap == OverflowWrap::BreakWord;
206
207        let mut last_slice = self.range.start..self.range.start;
208        for break_index in linebreak_iter {
209            if *break_index == self.range.start {
210                self.break_at_start = true;
211                continue;
212            }
213
214            let mut options = *shaping_options;
215
216            // Extend the slice to the next UAX#14 line break opportunity.
217            let mut slice = last_slice.end..*break_index;
218            let word = &formatting_context_text[slice.clone()];
219
220            // Split off any trailing whitespace into a separate glyph run.
221            let mut whitespace = slice.end..slice.end;
222            let mut rev_char_indices = word.char_indices().rev().peekable();
223
224            let mut ends_with_whitespace = false;
225            let ends_with_newline = rev_char_indices
226                .peek()
227                .is_some_and(|&(_, character)| character == '\n');
228            if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices
229                .take_while(|&(_, character)| char_is_whitespace(character))
230                .last()
231            {
232                ends_with_whitespace = true;
233                whitespace.start = slice.start + first_white_space_index;
234
235                // If line breaking for a piece of text that has `white-space-collapse: break-spaces` there
236                // is a line break opportunity *after* every preserved space, but not before. This means
237                // that we should not split off the first whitespace, unless that white-space is a preserved
238                // newline.
239                //
240                // An exception to this is if the style tells us that we can break in the middle of words.
241                if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces &&
242                    first_white_space_character != '\n' &&
243                    !can_break_anywhere
244                {
245                    whitespace.start += first_white_space_character.len_utf8();
246                    options
247                        .flags
248                        .insert(ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG);
249                }
250
251                slice.end = whitespace.start;
252            }
253
254            // If there's no whitespace and `word-break` is set to `keep-all`, try increasing the slice.
255            // TODO: This should only happen for CJK text.
256            if !ends_with_whitespace &&
257                *break_index != self.range.end &&
258                text_style.word_break == WordBreak::KeepAll &&
259                !can_break_anywhere
260            {
261                continue;
262            }
263
264            // Only advance the last slice if we are not going to try to expand the slice.
265            last_slice = slice.start..*break_index;
266
267            // Push the non-whitespace part of the range.
268            if !slice.is_empty() {
269                self.shape_and_push_range(&slice, formatting_context_text, &options);
270            }
271
272            if whitespace.is_empty() {
273                continue;
274            }
275
276            options.flags.insert(
277                ShapingFlags::IS_WHITESPACE_SHAPING_FLAG |
278                    ShapingFlags::ENDS_WITH_WHITESPACE_SHAPING_FLAG,
279            );
280
281            // If `white-space-collapse: break-spaces` is active, insert a line breaking opportunity
282            // between each white space character in the white space that we trimmed off.
283            if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces {
284                let start_index = whitespace.start;
285                for (index, character) in formatting_context_text[whitespace].char_indices() {
286                    let index = start_index + index;
287                    self.shape_and_push_range(
288                        &(index..index + character.len_utf8()),
289                        formatting_context_text,
290                        &options,
291                    );
292                }
293                continue;
294            }
295
296            // The breaker breaks after every newline, so either there is none,
297            // or there is exactly one at the very end. In the latter case,
298            // split it into a different run. That's because shaping considers
299            // a newline to have the same advance as a space, but during layout
300            // we want to treat the newline as having no advance.
301            if ends_with_newline && whitespace.len() > 1 {
302                self.shape_and_push_range(
303                    &(whitespace.start..whitespace.end - 1),
304                    formatting_context_text,
305                    &options,
306                );
307                self.shape_and_push_range(
308                    &(whitespace.end - 1..whitespace.end),
309                    formatting_context_text,
310                    &options,
311                );
312            } else {
313                self.shape_and_push_range(&whitespace, formatting_context_text, &options);
314            }
315        }
316    }
317}
318
319/// A single [`TextRun`] for the box tree. These are all descendants of
320/// [`super::InlineBox`] or the root of the [`super::InlineFormattingContext`].  During
321/// box tree construction, text is split into [`TextRun`]s based on their font, script,
322/// etc. When these are created text is already shaped.
323///
324/// <https://www.w3.org/TR/css-display-3/#css-text-run>
325#[derive(Debug, MallocSizeOf)]
326pub(crate) struct TextRun {
327    /// The [`BaseFragmentInfo`] for this [`TextRun`]. Usually this comes from the
328    /// original text node in the DOM for the text.
329    pub base_fragment_info: BaseFragmentInfo,
330
331    /// A weak reference to the parent of this layout box. This becomes valid as soon
332    /// as the *parent* of this box is added to the tree.
333    pub parent_box: Option<WeakLayoutBox>,
334
335    /// The [`crate::SharedStyle`] from this [`TextRun`]s parent element. This is
336    /// shared so that incremental layout can simply update the parent element and
337    /// this [`TextRun`] will be updated automatically.
338    pub inline_styles: SharedInlineStyles,
339
340    /// The range of text in [`super::InlineFormattingContext::text_content`] of the
341    /// [`super::InlineFormattingContext`] that owns this [`TextRun`]. These are UTF-8 offsets.
342    pub text_range: Range<usize>,
343
344    /// The range of characters in this text in [`super::InlineFormattingContext::text_content`]
345    /// of the [`super::InlineFormattingContext`] that owns this [`TextRun`]. These are *not*
346    /// UTF-8 offsets.
347    pub character_range: Range<usize>,
348
349    /// The text of this [`TextRun`] with a font selected, broken into unbreakable
350    /// segments, and shaped.
351    pub shaped_text: Vec<TextRunSegment>,
352}
353
354impl TextRun {
355    pub(crate) fn new(
356        base_fragment_info: BaseFragmentInfo,
357        inline_styles: SharedInlineStyles,
358        text_range: Range<usize>,
359        character_range: Range<usize>,
360    ) -> Self {
361        Self {
362            base_fragment_info,
363            parent_box: None,
364            inline_styles,
365            text_range,
366            character_range,
367            shaped_text: Vec::new(),
368        }
369    }
370
371    pub(super) fn segment_and_shape(
372        &mut self,
373        formatting_context_text: &str,
374        layout_context: &LayoutContext,
375        linebreaker: &mut LineBreaker,
376        bidi_info: &BidiInfo,
377    ) {
378        let parent_style = self.inline_styles.style.borrow().clone();
379        let inherited_text_style = parent_style.get_inherited_text().clone();
380        let letter_spacing = inherited_text_style
381            .letter_spacing
382            .0
383            .resolve(parent_style.clone_font().font_size.computed_size());
384        let letter_spacing = if letter_spacing.px() != 0. {
385            Some(app_units::Au::from(letter_spacing))
386        } else {
387            None
388        };
389        let language = parent_style
390            .get_font()
391            ._x_lang
392            .0
393            .parse()
394            .unwrap_or(Language::UND);
395
396        let mut flags = ShapingFlags::empty();
397        if inherited_text_style.text_rendering == TextRendering::Optimizespeed {
398            flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG);
399            flags.insert(ShapingFlags::DISABLE_KERNING_SHAPING_FLAG)
400        }
401
402        let specified_word_spacing = &inherited_text_style.word_spacing;
403        let style_word_spacing: Option<Au> = specified_word_spacing.to_length().map(|l| l.into());
404
405        let segments = self
406            .segment_text_by_font(
407                layout_context,
408                formatting_context_text,
409                bidi_info,
410                &parent_style,
411            )
412            .into_iter()
413            .map(|mut segment| {
414                let word_spacing = style_word_spacing.unwrap_or_else(|| {
415                    let space_width = segment
416                        .info
417                        .font
418                        .glyph_index(' ')
419                        .map(|glyph_id| segment.info.font.glyph_h_advance(glyph_id))
420                        .unwrap_or(LAST_RESORT_GLYPH_ADVANCE);
421                    specified_word_spacing.to_used_value(Au::from_f64_px(space_width))
422                });
423
424                let mut flags = flags;
425                if segment.info.bidi_level.is_rtl() {
426                    flags.insert(ShapingFlags::RTL_FLAG);
427                }
428
429                // From https://www.w3.org/TR/css-text-3/#cursive-script:
430                // Cursive scripts do not admit gaps between their letters for either
431                // justification or letter-spacing.
432                let letter_spacing = if is_cursive_script(segment.info.script) {
433                    None
434                } else {
435                    letter_spacing
436                };
437                if letter_spacing.is_some() {
438                    flags.insert(ShapingFlags::IGNORE_LIGATURES_SHAPING_FLAG);
439                };
440
441                let shaping_options = ShapingOptions {
442                    letter_spacing,
443                    word_spacing,
444                    script: segment.info.script,
445                    language,
446                    flags,
447                };
448
449                segment.shape_text(
450                    &parent_style,
451                    formatting_context_text,
452                    linebreaker,
453                    &shaping_options,
454                );
455
456                segment
457            })
458            .collect();
459
460        let _ = std::mem::replace(&mut self.shaped_text, segments);
461    }
462
463    /// Take the [`TextRun`]'s text and turn it into [`TextRunSegment`]s. Each segment has a matched
464    /// font and script. Fonts may differ when glyphs are found in fallback fonts.
465    /// [`super::InlineFormattingContext`].
466    fn segment_text_by_font(
467        &mut self,
468        layout_context: &LayoutContext,
469        formatting_context_text: &str,
470        bidi_info: &BidiInfo,
471        parent_style: &ServoArc<ComputedValues>,
472    ) -> Vec<TextRunSegment> {
473        let font_group = layout_context
474            .font_context
475            .font_group(parent_style.clone_font());
476        let mut current: Option<TextRunSegment> = None;
477        let mut results = Vec::new();
478
479        let lang = parent_style.get_font()._x_lang.clone();
480        let text_run_text = &formatting_context_text[self.text_range.clone()];
481        let char_iterator = TwoCharsAtATimeIterator::new(text_run_text.chars());
482
483        // The next current character index within the entire inline formatting context's text.
484        let mut next_character_index = self.character_range.start;
485        // The next bytes index of the charcter within the entire inline formatting context's text.
486        let mut next_byte_index = self.text_range.start;
487
488        for (character, next_character) in char_iterator {
489            let current_character_index = next_character_index;
490            next_character_index += 1;
491
492            let current_byte_index = next_byte_index;
493            next_byte_index += character.len_utf8();
494
495            if char_does_not_change_font(character) {
496                continue;
497            }
498
499            let Some(font) = font_group.find_by_codepoint(
500                &layout_context.font_context,
501                character,
502                next_character,
503                lang.clone(),
504            ) else {
505                continue;
506            };
507
508            let info = FontAndScriptInfo {
509                font,
510                script: Script::from(character),
511                bidi_level: bidi_info.levels[current_byte_index],
512            };
513
514            // If the existing segment is compatible with the character, keep going.
515            if let Some(current) = current.as_mut() {
516                if current.update_if_compatible(&info) {
517                    continue;
518                }
519            }
520
521            // Add the new segment and finish the existing one, if we had one. If the first
522            // characters in the run were control characters we may be creating the first
523            // segment in the middle of the run (ie the start should be the start of this
524            // text run's text).
525            let (start_byte_index, start_character_index) = match current {
526                Some(_) => (current_byte_index, current_character_index),
527                None => (self.text_range.start, self.character_range.start),
528            };
529            let new = TextRunSegment::new(Arc::new(info), start_byte_index, start_character_index);
530            if let Some(mut finished) = current.replace(new) {
531                // The end of the previous segment is the start of the next one.
532                finished.range.end = current_byte_index;
533                finished.character_range.end = current_character_index;
534                results.push(finished);
535            }
536        }
537
538        // Either we have a current segment or we only had control characters and whitespace. In both
539        // of those cases, just use the first font.
540        if current.is_none() {
541            current = font_group.first(&layout_context.font_context).map(|font| {
542                TextRunSegment::new(
543                    Arc::new(FontAndScriptInfo {
544                        font,
545                        script: Script::Common,
546                        bidi_level: Level::ltr(),
547                    }),
548                    self.text_range.start,
549                    self.character_range.start,
550                )
551            })
552        }
553
554        // Extend the last segment to the end of the string and add it to the results.
555        if let Some(mut last_segment) = current.take() {
556            last_segment.range.end = self.text_range.end;
557            last_segment.character_range.end = self.character_range.end;
558            results.push(last_segment);
559        }
560
561        results
562    }
563
564    pub(super) fn layout_into_line_items(&self, ifc: &mut InlineFormattingContextLayout) {
565        if self.text_range.is_empty() {
566            return;
567        }
568
569        // If we are following replaced content, we should have a soft wrap opportunity, unless the
570        // first character of this `TextRun` prevents that soft wrap opportunity. If we see such a
571        // character it should also override the LineBreaker's indication to break at the start.
572        let have_deferred_soft_wrap_opportunity =
573            mem::replace(&mut ifc.have_deferred_soft_wrap_opportunity, false);
574        let mut soft_wrap_policy = match have_deferred_soft_wrap_opportunity {
575            true => SegmentStartSoftWrapPolicy::Force,
576            false => SegmentStartSoftWrapPolicy::FollowLinebreaker,
577        };
578
579        for segment in self.shaped_text.iter() {
580            segment.layout_into_line_items(self, soft_wrap_policy, ifc);
581            soft_wrap_policy = SegmentStartSoftWrapPolicy::FollowLinebreaker;
582        }
583    }
584}
585
586/// From <https://www.w3.org/TR/css-text-3/#cursive-script>:
587/// Cursive scripts do not admit gaps between their letters for either justification
588/// or letter-spacing. The following Unicode scripts are included: Arabic, Hanifi
589/// Rohingya, Mandaic, Mongolian, N’Ko, Phags Pa, Syriac
590fn is_cursive_script(script: Script) -> bool {
591    matches!(
592        script,
593        Script::Arabic |
594            Script::Hanifi_Rohingya |
595            Script::Mandaic |
596            Script::Mongolian |
597            Script::Nko |
598            Script::Phags_Pa |
599            Script::Syriac
600    )
601}
602
603/// Whether or not this character should be able to change the font during segmentation.  Certain
604/// character are not rendered at all, so it doesn't matter what font we use to render them. They
605/// should just be added to the current segment.
606fn char_does_not_change_font(character: char) -> bool {
607    if character.is_control() {
608        return true;
609    }
610    if character == '\u{00A0}' {
611        return true;
612    }
613    if is_bidi_control(character) {
614        return false;
615    }
616
617    let class = linebreak_property(character);
618    class == XI_LINE_BREAKING_CLASS_CM ||
619        class == XI_LINE_BREAKING_CLASS_GL ||
620        class == XI_LINE_BREAKING_CLASS_ZW ||
621        class == XI_LINE_BREAKING_CLASS_WJ ||
622        class == XI_LINE_BREAKING_CLASS_ZWJ
623}
624
625pub(super) fn get_font_for_first_font_for_style(
626    style: &ComputedValues,
627    font_context: &FontContext,
628) -> Option<FontRef> {
629    let font = font_context
630        .font_group(style.clone_font())
631        .first(font_context);
632    if font.is_none() {
633        warn!("Could not find font for style: {:?}", style.clone_font());
634    }
635    font
636}
637pub(crate) struct TwoCharsAtATimeIterator<InputIterator> {
638    /// The input character iterator.
639    iterator: InputIterator,
640    /// The first character to produce in the next run of the iterator.
641    next_character: Option<char>,
642}
643
644impl<InputIterator> TwoCharsAtATimeIterator<InputIterator> {
645    fn new(iterator: InputIterator) -> Self {
646        Self {
647            iterator,
648            next_character: None,
649        }
650    }
651}
652
653impl<InputIterator> Iterator for TwoCharsAtATimeIterator<InputIterator>
654where
655    InputIterator: Iterator<Item = char>,
656{
657    type Item = (char, Option<char>);
658
659    fn next(&mut self) -> Option<Self::Item> {
660        // If the iterator isn't initialized do that now.
661        if self.next_character.is_none() {
662            self.next_character = self.iterator.next();
663        }
664        let character = self.next_character?;
665        self.next_character = self.iterator.next();
666        Some((character, self.next_character))
667    }
668}