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