Skip to main content

script/drag/
document_selection_drag.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::cmp::Ordering;
6
7use js::context::{JSContext, NoGC};
8use layout_api::SegmentGranularity;
9use rustc_hash::FxHashMap;
10use script_bindings::dom::UnrootedDom;
11use script_bindings::inheritance::Castable;
12use script_bindings::root::{Dom, DomRoot};
13use style::values::computed::UserSelect;
14
15use crate::dom::inputevent::HitTestResult;
16use crate::dom::selection::UsedUserSelect;
17use crate::dom::selection_range::{RootedSelectionBoundary, SelectionRange};
18use crate::dom::{Document, Element, Node};
19
20/// Information stored when the start of the drag was triggered via double or triple
21/// click which highlighted a segment of text.
22#[derive(JSTraceable, MallocSizeOf)]
23#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
24pub(crate) struct DocumentDragSegment {
25    /// The granularity of the selection drag.
26    #[no_trace]
27    pub granularity: SegmentGranularity,
28    /// The original segment that was highlighted at the start of the drag.
29    pub range: SelectionRange,
30}
31
32#[derive(JSTraceable, MallocSizeOf)]
33#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
34pub(crate) struct DocumentSelectionDragHandler {
35    /// The [`Document`] of this [`DocumentSelectionDragHandler`].
36    document: Dom<Document>,
37    /// The node with `user-select: contain` that this selection must not leave, if any.
38    user_select_contain_node_for_selection_anchor: Option<Dom<Node>>,
39    /// Information when the start of the drag highlighted a segment of text.
40    segment: Option<DocumentDragSegment>,
41}
42
43impl DocumentSelectionDragHandler {
44    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
45    pub(crate) fn new(
46        document: &Document,
47        user_select_contain_node: Option<&Node>,
48        segment: Option<DocumentDragSegment>,
49    ) -> Self {
50        Self {
51            document: Dom::from_ref(document),
52            user_select_contain_node_for_selection_anchor: user_select_contain_node
53                .map(Dom::from_ref),
54            segment,
55        }
56    }
57
58    pub(crate) fn still_connected(&self) -> bool {
59        self.user_select_contain_node_for_selection_anchor
60            .as_ref()
61            .is_none_or(|node| node.is_connected())
62    }
63
64    /// Process a mouse move event on this [`DocumentSelectionDragHandler`].
65    ///
66    /// Returns `true` if the drag should continue and `false` otherwise.
67    pub(crate) fn moved(&self, cx: &mut JSContext, hit_test_result: &HitTestResult) -> bool {
68        let Some(selection) = self.document.selection() else {
69            return true;
70        };
71
72        let Some(boundary) = hit_test_result.dom_position_for_selection.as_ref() else {
73            return true;
74        };
75
76        let adjust_for_user_select =
77            |anchor: Option<&RootedSelectionBoundary>, focus: RootedSelectionBoundary| {
78                adjust_focus_for_user_select(
79                    cx,
80                    anchor,
81                    focus,
82                    self.user_select_contain_node_for_selection_anchor
83                        .as_deref(),
84                )
85            };
86
87        let Some(segment) = &self.segment else {
88            selection.collapse_or_extend_to_dom_position(
89                cx,
90                &adjust_for_user_select(
91                    selection.composed_anchor_position().as_ref(),
92                    boundary.clone(),
93                ),
94            );
95            return true;
96        };
97
98        let (start, end) = self
99            .document
100            .window()
101            .query_text_segment(
102                &boundary.container,
103                boundary.utf32_offset(),
104                segment.granularity,
105            )
106            .unwrap_or_else(|| (boundary.clone(), boundary.clone()));
107
108        let original_start = segment.range.start.rooted();
109        let original_end = segment.range.end.rooted();
110        let (anchor, focus) = if boundary.cmp(cx.no_gc(), &original_start) == Ordering::Less {
111            // If the drag position is before the original starting point, then the newly
112            // selected text extends from the old end to the start of the drag segment.
113            (original_end, start)
114        } else if boundary.cmp(cx.no_gc(), &original_end) == Ordering::Greater {
115            // If the drag position is after the original end, then the newly selected
116            // text extends from the original start to the end of the drag segment.
117            (original_start, end)
118        } else {
119            // This is when we are dragging within the original segment and want to keep
120            // it selected.
121            (original_start, original_end)
122        };
123
124        let focus = adjust_for_user_select(Some(&anchor), focus);
125        selection.set_anchor_and_focus(cx, &anchor, &focus);
126        true
127    }
128}
129
130/// Adjust the range boundary for the selection anchor (start of a drag gesture)
131/// based on [`user-select`] of the container node and of its ancestors.
132///
133/// Returns `None` if no new drag gesture should be started (`user-select: none`), or:
134///
135/// * The new anchor container
136/// * The new anchor offset
137/// * The node with `user-select: contain` that the selection must not leave, if any
138///
139/// [`user-select`]: https://drafts.csswg.org/css-ui-4/#content-selection
140pub(crate) fn adjust_anchor_for_user_select(
141    no_gc: &NoGC,
142    mut anchor_candidate: RootedSelectionBoundary,
143) -> Option<(RootedSelectionBoundary, Option<DomRoot<Node>>)> {
144    let mut cache = Default::default();
145    if anchor_candidate
146        .container
147        .used_user_select(no_gc, &mut cache) ==
148        UsedUserSelect::None
149    {
150        return None;
151    }
152
153    // The nearest inclusive ancestor with `user-select: contain`
154    let mut nearest_with_user_select_contain = None;
155
156    // The furthest inclusive ancestor with `user-select: all`, as long as all intermediate
157    // inclusive ancestors also have `user-select: all`.
158    let mut furthest_with_user_select_all = None;
159    let mut each_so_far_has_user_select_all = true;
160
161    for ancestor in anchor_candidate
162        .container
163        .inclusive_ancestors_in_flat_tree_unrooted(no_gc)
164    {
165        match ancestor.used_user_select(no_gc, &mut cache) {
166            UsedUserSelect::Text | UsedUserSelect::None => {
167                each_so_far_has_user_select_all = false;
168            },
169            UsedUserSelect::Contain => {
170                nearest_with_user_select_contain = Some(ancestor.as_rooted());
171                // `each_so_far_has_user_select_all` should be set to false but will no
172                // longer be used after this break.
173                break;
174            },
175            UsedUserSelect::All => {
176                if each_so_far_has_user_select_all {
177                    furthest_with_user_select_all = Some(ancestor);
178                }
179            },
180        }
181    }
182
183    if let Some(atomic) = furthest_with_user_select_all {
184        // TODO: to properly implement `user-select: all` we should keep track of two potential
185        // start boundaries: one with offset zero as is done here, and another with offset
186        // `Node::len(atomic)` representing the end of that node. The latter would be used
187        // when the range is backwards (if the end boundary is outside of and before `atomic`)
188        // so that `atomic` would be entirely selected regardless of the range direction.
189        anchor_candidate = RootedSelectionBoundary::start_of(&atomic);
190    }
191    Some((anchor_candidate, nearest_with_user_select_contain))
192}
193
194/// Adjust the range boundary for the selection focus (end of a drag gesture)
195/// based on [`user-select`] of the container node and of its ancestors.
196///
197/// [`user-select`]: https://drafts.csswg.org/css-ui-4/#content-selection
198pub(crate) fn adjust_focus_for_user_select(
199    no_gc: &NoGC,
200    anchor: Option<&RootedSelectionBoundary>,
201    mut focus_candidate: RootedSelectionBoundary,
202    user_select_contain_node_for_anchor: Option<&Node>,
203) -> RootedSelectionBoundary {
204    let mut cache = Default::default();
205    let mut user_select_contain_node_for_anchor_is_inclusive_ancestor = false;
206    let mut each_so_far_has_user_select_all = true;
207    let mut furthest_node_with_user_select_all = None;
208    let mut each_so_far_has_user_select_none = true;
209    // Either `user-select: contain` the selection starts outside of, or `user-select: none`
210    let mut furthest_node_to_avoid = None;
211
212    for inclusive_ancestor in focus_candidate
213        .container
214        .inclusive_ancestors_in_flat_tree_unrooted(no_gc)
215    {
216        if user_select_contain_node_for_anchor == Some(&**inclusive_ancestor) {
217            user_select_contain_node_for_anchor_is_inclusive_ancestor = true
218        }
219        match inclusive_ancestor.used_user_select(no_gc, &mut cache) {
220            UsedUserSelect::Text => {
221                each_so_far_has_user_select_all = false;
222                each_so_far_has_user_select_none = false;
223            },
224            UsedUserSelect::None => {
225                each_so_far_has_user_select_all = false;
226                if each_so_far_has_user_select_none {
227                    furthest_node_to_avoid = Some(inclusive_ancestor)
228                }
229            },
230            UsedUserSelect::Contain => {
231                each_so_far_has_user_select_all = false;
232                each_so_far_has_user_select_none = false;
233                if !user_select_contain_node_for_anchor_is_inclusive_ancestor {
234                    furthest_node_to_avoid = Some(inclusive_ancestor)
235                }
236            },
237            UsedUserSelect::All => {
238                each_so_far_has_user_select_none = false;
239                if each_so_far_has_user_select_all {
240                    furthest_node_with_user_select_all = Some(inclusive_ancestor)
241                }
242            },
243        }
244        if !each_so_far_has_user_select_all &&
245            !each_so_far_has_user_select_none &&
246            user_select_contain_node_for_anchor_is_inclusive_ancestor
247        {
248            // Nothing else to find in ancestors
249            break;
250        }
251    }
252    let selection_is_backward = || {
253        anchor.is_some_and(|anchor| {
254            anchor.compare_dom_positions(no_gc, &focus_candidate) == Some(Ordering::Greater)
255        })
256    };
257    if let Some(contain_for_anchor) = user_select_contain_node_for_anchor &&
258        !user_select_contain_node_for_anchor_is_inclusive_ancestor
259    {
260        // `focus_container` is outside of `contain_for_anchor`:
261        // find the closest position within `contain_for_anchor`: either its start or end
262        focus_candidate = if selection_is_backward() {
263            RootedSelectionBoundary::start_of(contain_for_anchor)
264        } else {
265            RootedSelectionBoundary::end_of(contain_for_anchor)
266        };
267    } else if let Some(focus_ancestor_to_avoid) = furthest_node_to_avoid {
268        focus_candidate = if selection_is_backward() {
269            RootedSelectionBoundary::end_of(&focus_ancestor_to_avoid)
270        } else {
271            RootedSelectionBoundary::start_of(&focus_ancestor_to_avoid)
272        };
273    } else if let Some(with_user_select_all) = furthest_node_with_user_select_all {
274        // anchor was snapped to the start of the `user-select: all` element,
275        // so snap the focus to the end unconditionally
276        focus_candidate = RootedSelectionBoundary::end_of(&with_user_select_all);
277    }
278
279    focus_candidate
280}
281
282impl Node {
283    /// Returns the used value of <https://drafts.csswg.org/css-ui-4/#propdef-user-select>.
284    ///
285    /// It is the caller’s responsibility to ensure that style is up to date for this node and
286    /// its (flat tree) ancestors.
287    ///
288    /// `cache` can be initialized with `Default::default()`, and should be shared across calls
289    /// for nodes that may share some (flat tree) ancestors.
290    pub(crate) fn used_user_select<'no_gc>(
291        &self,
292        no_gc: &'no_gc NoGC,
293        cache: &mut FxHashMap<UnrootedDom<'no_gc, Node>, UsedUserSelect>,
294    ) -> UsedUserSelect {
295        let cache_key = UnrootedDom::from_ref(self, no_gc);
296        if let Some(&used_value) = cache.get(&cache_key) {
297            return used_value;
298        }
299        // https://html.spec.whatwg.org/multipage/#inert
300        // > When a node is inert: Text selection functionality must act as if
301        // > the 'user-select' CSS property were set to 'none'.
302        if self.is_inert() {
303            cache.insert(cache_key, UsedUserSelect::None);
304            return UsedUserSelect::None;
305        }
306        // > The used value is the same as the computed value, except:
307        // >
308        // > 1. on editable elements where the used value is always `contain`
309        // >    regardless of the computed value
310        // > 2. when the computed value is `auto`, in which case the used value
311        // >    is one of the other values as defined below
312        // >
313        // > For the purpose of this specification, an editable element is
314        // > either an editing host or a mutable form control with textual
315        // > content, such as textarea.
316        //
317        // For form controls, we handle selection separately without looking at
318        // `user-select`.
319        if self.is_editing_host() {
320            let used_value = UsedUserSelect::Contain;
321            cache.insert(cache_key, used_value);
322            return used_value;
323        }
324        let computed_value = self
325            .downcast()
326            .and_then(Element::computed_user_select)
327            // Non-element nodes and unstyled elements: use the initial value
328            .unwrap_or(UserSelect::Auto);
329        let used_value = match computed_value {
330            UserSelect::Text => UsedUserSelect::Text,
331            UserSelect::None => UsedUserSelect::None,
332            UserSelect::Contain => UsedUserSelect::Contain,
333            UserSelect::All => UsedUserSelect::All,
334            UserSelect::Auto => {
335                let parent_used_value = self
336                    .parent_in_flat_tree(no_gc)
337                    .into_parent()
338                    .map(|parent| parent.used_user_select(no_gc, cache));
339                // > The used value of `auto` is determined as follows:
340                // > * On the `::before` and `::after` pseudo-elements, the used value is `none`
341                // > * If the element is an editable element, the used value is `contain`
342                // > * Otherwise, if the used value of `user-select` on the parent of this element
343                // >   is `all`, the used value is `all`
344                // > * Otherwise, if the used value of `user-select` on the parent of this element
345                // >   is `none`, the used value is `none`
346                // > * Otherwise, the used value is `text`
347                match parent_used_value {
348                    Some(UsedUserSelect::All) => UsedUserSelect::All,
349                    Some(UsedUserSelect::None) => UsedUserSelect::None,
350                    _ => UsedUserSelect::Text,
351                }
352            },
353        };
354        cache.insert(cache_key, used_value);
355        used_value
356    }
357}