Skip to main content

layout/flow/inline/
shaping_queue.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::ops::Range;
6use std::sync::Arc;
7
8use fonts::{ShapedText, ShapedTextSlice, ShapedTextSliceType, ShapedTextSlicer, ShapingOptions};
9use icu_segmenter::LineBreakOptions;
10use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
11use style::computed_values::word_break::T as WordBreak;
12use style::properties::ComputedValues;
13use style::str::char_is_whitespace;
14use style::values::computed::OverflowWrap;
15use unicode_script::Script;
16
17use crate::ArcRefCell;
18use crate::flow::inline::line_breaker::LineBreaker;
19use crate::flow::inline::text_run::{FontAndScriptInfo, TextRun, TextRunItem, script_is_specific};
20
21/// An entry on the shaping queue that represents text that needs to be shaped.
22/// This contains a lot of duplicated data from `TextRunSegment` so that
23/// it can outlive a mutable borrow on the owning `TextRun`.
24pub(crate) struct ShapingQueueText {
25    info: FontAndScriptInfo,
26    byte_range: Range<usize>,
27    character_range: Range<usize>,
28    text_run: ArcRefCell<TextRun>,
29    index_in_text_run: usize,
30    old_shaped_text: Option<Arc<ShapedText>>,
31}
32
33/// A new entry for the [`ShapingQueue`].
34pub(crate) enum ShapingQueueEntry {
35    PreservedTabOrNewline,
36    Text(ShapingQueueText),
37}
38
39impl ShapingQueueEntry {
40    pub(crate) fn new(
41        text_run: ArcRefCell<TextRun>,
42        text_run_item: &TextRunItem,
43        index_in_text_run: usize,
44        old_text_run_line_item: Option<TextRunItem>,
45    ) -> Self {
46        let text_segment = match text_run_item {
47            TextRunItem::LineBreak { .. } | TextRunItem::Tab { .. } => {
48                return Self::PreservedTabOrNewline;
49            },
50            TextRunItem::TextSegment(text_run_segment) => text_run_segment,
51        };
52
53        let old_shaped_text = old_text_run_line_item.and_then(|old_text_run_line_item| {
54            let TextRunItem::TextSegment(old_text_segment) = old_text_run_line_item else {
55                return None;
56            };
57            if !text_segment.is_compatible_with_old_shaping_result(&old_text_segment) {
58                return None;
59            }
60            old_text_segment.shaped_text
61        });
62
63        Self::Text(ShapingQueueText {
64            info: text_segment.info.clone(),
65            byte_range: text_segment.byte_range.clone(),
66            character_range: text_segment.character_range.clone(),
67            text_run,
68            index_in_text_run,
69            old_shaped_text,
70        })
71    }
72}
73
74struct BatchSlicer<'a> {
75    slicer: ShapedTextSlicer,
76    text: &'a str,
77    line_breaker: &'a mut LineBreaker,
78    character_offset_origin: usize,
79}
80
81impl BatchSlicer<'_> {
82    fn slice_shaped_text_at_line_break_opportunities(
83        &mut self,
84        segment: &ShapingQueueText,
85        parent_style: &ComputedValues,
86    ) -> (Vec<Arc<ShapedTextSlice>>, bool) {
87        // Gather the linebreaks that apply to this segment from the inline formatting context's collection
88        // of line breaks. Also add a simulated break at the end of the segment in order to ensure the final
89        // piece of text is processed.
90        let range = segment.byte_range.clone();
91        let linebreaks = self
92            .line_breaker
93            .advance_to_linebreaks_in_range(segment.byte_range.clone());
94        let linebreak_iter = linebreaks.iter().chain(std::iter::once(&range.end));
95
96        let mut break_at_start = false;
97
98        let text_style = parent_style.get_inherited_text();
99        let can_break_anywhere = text_style.word_break == WordBreak::BreakAll ||
100            text_style.overflow_wrap == OverflowWrap::Anywhere ||
101            text_style.overflow_wrap == OverflowWrap::BreakWord;
102
103        let mut last_slice = segment.byte_range.start..segment.byte_range.start;
104        let mut current_character_offset =
105            segment.character_range.start - self.character_offset_origin;
106
107        let mut runs = Vec::with_capacity(linebreaks.len());
108        let mut maybe_push_run = |run: Option<Arc<ShapedTextSlice>>| {
109            if let Some(run) = run {
110                runs.push(run);
111            }
112        };
113
114        for break_index in linebreak_iter {
115            if *break_index == segment.byte_range.start {
116                break_at_start = true;
117                continue;
118            }
119
120            // Extend the slice to the next UAX#14 line break opportunity.
121            let mut slice = last_slice.end..*break_index;
122            let word = &self.text[slice.clone()];
123
124            // Split off any trailing whitespace into a separate glyph run.
125            let mut whitespace = slice.end..slice.end;
126            let rev_char_indices = word.char_indices().rev().peekable();
127
128            let mut slice_type = ShapedTextSliceType::Word;
129            let mut ends_with_whitespace = false;
130            if let Some((first_white_space_index, first_white_space_character)) = rev_char_indices
131                .take_while(|&(_, character)| char_is_whitespace(character))
132                .last()
133            {
134                ends_with_whitespace = true;
135                whitespace.start = slice.start + first_white_space_index;
136
137                // If line breaking for a piece of text that has `white-space-collapse:
138                // break-spaces` there is a line break opportunity *after* every preserved space,
139                // but not before. This means that we should not split off the first whitespace.
140                //
141                // An exception to this is if the style tells us that we can break in the middle of words.
142                if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces &&
143                    !can_break_anywhere
144                {
145                    whitespace.start += first_white_space_character.len_utf8();
146                    slice_type = ShapedTextSliceType::WordAndWhiteSpace;
147                }
148
149                slice.end = whitespace.start;
150            }
151
152            // If there's no whitespace and `word-break` is set to `keep-all`, try increasing the slice.
153            // TODO: This should only happen for CJK text.
154            if !ends_with_whitespace &&
155                *break_index != segment.byte_range.end &&
156                text_style.word_break == WordBreak::KeepAll &&
157                !can_break_anywhere
158            {
159                continue;
160            }
161
162            // Only advance the last slice if we are not going to try to expand the slice.
163            last_slice = slice.start..*break_index;
164
165            // Push the non-whitespace part of the range.
166            if !slice.is_empty() {
167                current_character_offset += self.text[slice].chars().count();
168                maybe_push_run(
169                    self.slicer
170                        .slice_until_character_offset(current_character_offset, slice_type),
171                );
172            }
173
174            if whitespace.is_empty() {
175                continue;
176            }
177
178            // If `white-space-collapse: break-spaces` is active, insert a line breaking opportunity
179            // between each white space character in the white space that we trimmed off.
180            if text_style.white_space_collapse == WhiteSpaceCollapse::BreakSpaces {
181                for _ in self.text[whitespace].chars() {
182                    current_character_offset += 1;
183                    maybe_push_run(self.slicer.slice_until_character_offset(
184                        current_character_offset,
185                        ShapedTextSliceType::WhiteSpace,
186                    ));
187                }
188                continue;
189            }
190
191            current_character_offset += self.text[whitespace].chars().count();
192            maybe_push_run(self.slicer.slice_until_character_offset(
193                current_character_offset,
194                ShapedTextSliceType::WhiteSpace,
195            ));
196        }
197
198        (runs, break_at_start)
199    }
200}
201
202/// The [`ShapingQueue`] is responsible for shaping text during inline formatting context
203/// construction. It allows for shaping text across inline box boundaries. When pushing
204/// items to the queue, if the items are compatible pieces of text that can be shaped
205/// together, they are accumulated. The queue may be flushed in the given situations:
206///
207/// - An incompatible piece of text (different fonts or certain style properties) is
208///   pushed to the queue.
209/// - A preserved newline or tab is pushed to the queue.
210/// - An inline box breaks shaping via padding, border, margins or a non-`baseline`
211///   `vertical-align` property.
212/// - Atomic content in the inline formatting context
213///
214/// Upon flushing, the [`ShapingQueue`] will shape any pending text and assign the
215/// resulting [`ShapedTextSlice`]s to the originating [`TextRun`]s.
216pub(crate) struct ShapingQueue<'a> {
217    /// The queue of items in the current batch that will be shaped together.
218    queue: Vec<ShapingQueueText>,
219    /// The text that will be used for shaping.
220    text: &'a str,
221    /// The line breaker that will be used to slice shaping results across on line break boundaries.
222    line_breaker: LineBreaker,
223    /// The byte range of the text to shape in [`Self::text`] for the current batch.
224    /// Only contiguous ranges can be shaped together.
225    byte_range: Range<usize>,
226    /// The character range of the text to shape in [`Self::text`] for the current batch.
227    /// Only contiguous ranges can be shaped together.
228    character_range: Range<usize>,
229    /// The resolved script for the current batch. This is used to gradually turn non-specific
230    /// scripts into a resolved value for shaping.
231    resolved_script: Option<Script>,
232}
233
234impl<'a> ShapingQueue<'a> {
235    pub(crate) fn new(text: &'a str, line_break_options: LineBreakOptions) -> Self {
236        Self {
237            queue: Default::default(),
238            text,
239            line_breaker: LineBreaker::new(text, line_break_options),
240            byte_range: Default::default(),
241            character_range: Default::default(),
242            resolved_script: None,
243        }
244    }
245
246    fn compatible_old_shaping_result(&self, character_count: usize) -> Option<Arc<ShapedText>> {
247        let old_shaped_text = self.queue.first()?.old_shaped_text.as_ref()?;
248        if old_shaped_text.character_count() != character_count {
249            return None;
250        }
251
252        if !self.queue.iter().all(|entry| {
253            entry
254                .old_shaped_text
255                .as_ref()
256                .is_some_and(|entry_old_shaped_text| {
257                    Arc::ptr_eq(old_shaped_text, entry_old_shaped_text)
258                })
259        }) {
260            return None;
261        }
262        Some(old_shaped_text.clone())
263    }
264
265    fn shape_batch(&self) -> Option<Arc<ShapedText>> {
266        let first = self.queue.first()?;
267
268        let character_count = self.character_range.end - self.character_range.start;
269        if let Some(old_shaping_result) = self.compatible_old_shaping_result(character_count) {
270            return Some(old_shaping_result);
271        };
272
273        let mut options: ShapingOptions = (&first.info).into();
274        options.script = self.resolved_script.unwrap_or(first.info.script);
275
276        let font = &first.info.font_info.font;
277        Some(font.shape_text(&self.text[self.byte_range.clone()], &options))
278    }
279
280    /// Flush this [`ShapingQueue`]. If any content had been collected up to this point,
281    /// it will be shaped and the resulting [`ShapedTextSlice`]s will be assigned to their
282    /// originating [`TextRun`]s.
283    pub(crate) fn flush(&mut self) {
284        let Some(shaped_text) = self.shape_batch() else {
285            return;
286        };
287
288        let mut slicer = BatchSlicer {
289            slicer: ShapedTextSlicer::new(shaped_text.clone()),
290            text: self.text,
291            line_breaker: &mut self.line_breaker,
292            character_offset_origin: self.character_range.start,
293        };
294
295        for entry in self.queue.drain(..) {
296            let mut text_run = entry.text_run.borrow_mut();
297            let style = text_run.inline_styles().style.borrow().clone();
298            let (runs, break_at_start) =
299                slicer.slice_shaped_text_at_line_break_opportunities(&entry, &style);
300
301            if let TextRunItem::TextSegment(text_segment) =
302                &mut text_run.items[entry.index_in_text_run]
303            {
304                text_segment.shaped_text = Some(shaped_text.clone());
305                text_segment.runs = runs;
306                text_segment.break_at_start = break_at_start;
307            }
308        }
309    }
310
311    fn compatible_with_batch(&self, text: &ShapingQueueText) -> bool {
312        // If the queue is empty, we can always add new text to the batch.
313        let Some(last) = self.queue.last() else {
314            return true;
315        };
316
317        // The new text is only compatible with the current batch if their character and
318        // text byte boundaries are contiguous.
319        if last.character_range.end != text.character_range.start ||
320            last.byte_range.end != text.byte_range.start
321        {
322            return false;
323        }
324
325        // The `FontInfo`s of the batch and the new text need to match exactly to shape
326        // together.
327        if !Arc::ptr_eq(&last.info.font_info, &text.info.font_info) &&
328            *last.info.font_info != *text.info.font_info
329        {
330            return false;
331        }
332
333        // Any resolved `Script` has to be compatible with any new specific `Script`.
334        !script_is_specific(text.info.script) ||
335            self.resolved_script
336                .is_none_or(|resolved_script| resolved_script == text.info.script)
337    }
338
339    fn push_text(&mut self, text: ShapingQueueText) {
340        if !self.compatible_with_batch(&text) {
341            self.flush();
342        }
343
344        if self.queue.is_empty() {
345            self.character_range = text.character_range.clone();
346            self.byte_range = text.byte_range.clone();
347            self.resolved_script = None;
348        } else {
349            self.character_range.end = text.character_range.end;
350            self.byte_range.end = text.byte_range.end;
351        }
352        if self.resolved_script.is_none() && script_is_specific(text.info.script) {
353            self.resolved_script = Some(text.info.script);
354        }
355
356        self.queue.push(text);
357    }
358
359    /// Push a new [`ShapingQueueEntry`] on to this [`ShapingQueue`], maybe flushing
360    /// previously collected entries.
361    pub(crate) fn push(&mut self, entry: ShapingQueueEntry) {
362        match entry {
363            ShapingQueueEntry::PreservedTabOrNewline => self.flush(),
364            ShapingQueueEntry::Text(shaping_queue_text) => self.push_text(shaping_queue_text),
365        }
366    }
367}