Skip to main content

layout/
selection.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;
6
7use icu_segmenter::WordSegmenter;
8use itertools::Either;
9use layout_api::SegmentGranularity;
10use script::layout_dom::ServoLayoutNode;
11use servo_base::text::{AssumeUnder4GB, Utf32CodeUnits, Utf32CodeUnitsOrNodeOffset};
12use style::dom::OpaqueNode;
13
14use crate::ArcRefCell;
15use crate::dom::{LayoutBox, NodeExt};
16use crate::flow::inline::InlineFormattingContext;
17use crate::flow::inline::text_run::TextRun;
18
19/// An iterator that turns boundaries in UTF-8 code unit offsets into UTF-32 code unit ranges.
20/// Empty ranges are skipped.
21struct BoundaryIterator<'a, Boundaries> {
22    boundaries: Boundaries,
23    text: &'a str,
24    previous_boundary: usize,
25    previous_boundary_in_characters: Utf32CodeUnits,
26}
27
28impl<'a, Boundaries> BoundaryIterator<'a, Boundaries> {
29    fn new(text: &'a str, boundaries: Boundaries) -> Self {
30        Self {
31            boundaries,
32            text,
33            previous_boundary: 0,
34            previous_boundary_in_characters: Utf32CodeUnits(0),
35        }
36    }
37}
38
39impl<Boundaries: Iterator<Item = usize>> Iterator for BoundaryIterator<'_, Boundaries> {
40    type Item = Range<Utf32CodeUnits>;
41
42    fn next(&mut self) -> Option<Self::Item> {
43        let mut next_boundary = self.boundaries.next()?;
44        while next_boundary == self.previous_boundary {
45            next_boundary = self.boundaries.next()?;
46        }
47
48        let start = self.previous_boundary_in_characters;
49        let end = start +
50            Utf32CodeUnits::length_of(
51                AssumeUnder4GB,
52                &self.text[self.previous_boundary..next_boundary],
53            );
54        self.previous_boundary = next_boundary;
55        self.previous_boundary_in_characters = end;
56        Some(start..end)
57    }
58}
59
60pub(crate) struct PositionInInlineFormattingContext {
61    /// The [`LayoutBox`] of the containing inline formatting context box.
62    container: LayoutBox,
63    /// The offset of this position in the inline formatting context's transformed text.
64    offset: Utf32CodeUnits,
65    /// The [`TextRun`] that contains this position.
66    text_run: ArcRefCell<TextRun>,
67}
68
69impl PositionInInlineFormattingContext {
70    /// If the given text node and a DOM offset in that node are contained within an
71    /// inline formatting context, create a [`PositionInInlineFormattingContext`] for
72    /// them. If the node is not a text node or the containing inline formatting context
73    /// cannot be found return `None`.
74    pub(crate) fn from_dom_position_in_text_node(
75        node: &ServoLayoutNode,
76        offset: Utf32CodeUnitsOrNodeOffset,
77    ) -> Option<Self> {
78        let inner_layout_data = node.inner_layout_data();
79        let self_box = inner_layout_data.as_ref()?.self_box.borrow();
80        let layout_box = self_box.as_ref()?;
81
82        let LayoutBox::Text(text_run) = layout_box else {
83            return None;
84        };
85        let offset = text_run
86            .borrow()
87            .run_data
88            .map_dom_offset_to_transformed_ifc_offset(Utf32CodeUnits(offset.0));
89        let container = layout_box.layout_box_for_containing_inline_formatting_context()?;
90        Some(Self {
91            container,
92            offset,
93            text_run: text_run.clone(),
94        })
95    }
96
97    /// Return the offsets of the segment with the given [`SegmentGranularity`] at this position.
98    fn offsets(&self, granularity: SegmentGranularity) -> Range<Utf32CodeUnits> {
99        let text_run = self.text_run.borrow();
100        let run_data = &text_run.run_data;
101        let text = run_data
102            .text_content
103            .get()
104            .expect("There should always be text at this point");
105
106        let iterator = || match granularity {
107            SegmentGranularity::Word => Either::Left(BoundaryIterator::new(
108                text,
109                WordSegmenter::new_auto(Default::default()).segment_str(text),
110            )),
111            SegmentGranularity::HardLineBreak => Either::Right(BoundaryIterator::new(
112                text,
113                text.split_inclusive("\n").scan(0, |offset, chunk| {
114                    *offset += chunk.len();
115                    Some(*offset)
116                }),
117            )),
118        };
119
120        iterator()
121            .find(|range| range.contains(&self.offset))
122            .or_else(|| iterator().last())
123            .unwrap_or_else(|| run_data.character_range_in_ifc_text.clone())
124    }
125
126    /// Find the segment boundaries of the segment with the given [`SegmentGranularity`]
127    /// at this position.
128    pub(crate) fn segment(
129        &self,
130        granularity: SegmentGranularity,
131    ) -> Option<(
132        (OpaqueNode, Utf32CodeUnitsOrNodeOffset),
133        (OpaqueNode, Utf32CodeUnitsOrNodeOffset),
134    )> {
135        let range = self.offsets(granularity);
136        self.container
137            .with_inline_formatting_context(|inline_formatting_context| {
138                let start = inline_formatting_context
139                    .find_dom_position_of_offset(range.start, SearchAffinity::RangeStart)?;
140                let end = inline_formatting_context
141                    .find_dom_position_of_offset(range.end, SearchAffinity::RangeEnd)?;
142                Some((start, end))
143            })
144    }
145}
146
147/// Decides which way to bias a search for an offset in an inline formatting context.
148enum SearchAffinity {
149    /// Search for the first offset after the given offset, and fall back to the end of
150    /// the last text run when searching for an offset.
151    RangeStart,
152    /// Search for the last offset before the given offset and fall back to the start of the
153    /// first text run when searching for an offset.
154    RangeEnd,
155}
156
157impl InlineFormattingContext {
158    /// For the given UTF-32 offset in transformed inline formatting text, find the text
159    /// node and DOM text offset that best corresponds to it.
160    fn find_dom_position_of_offset(
161        &self,
162        offset: Utf32CodeUnits,
163        search_direction: SearchAffinity,
164    ) -> Option<(OpaqueNode, Utf32CodeUnitsOrNodeOffset)> {
165        let mut text_runs = self.non_pseudo_text_runs();
166        let text_run = match search_direction {
167            SearchAffinity::RangeStart => text_runs
168                .find(|text_run| offset < text_run.run_data.character_range_in_ifc_text.end)
169                .or_else(|| self.non_pseudo_text_runs().next_back()),
170            SearchAffinity::RangeEnd => text_runs
171                .rev()
172                .find(|text_run| offset > text_run.run_data.character_range_in_ifc_text.start)
173                .or_else(|| self.non_pseudo_text_runs().next()),
174        }?;
175
176        let mapped_offset = text_run
177            .run_data
178            .map_transformed_ifc_offset_to_dom_offset(offset);
179        Some((
180            text_run.base_fragment_info.tag?.node,
181            Utf32CodeUnitsOrNodeOffset(mapped_offset.0),
182        ))
183    }
184}