Skip to main content

script/dom/
scrolling_box.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::cell::Cell;
6
7use app_units::Au;
8use euclid::{Rect, Vector2D};
9use js::context::JSContext;
10use layout_api::{AxesOverflow, ScrollContainerQueryFlags};
11use script_bindings::codegen::GenericBindings::WindowBinding::ScrollBehavior;
12use script_bindings::inheritance::Castable;
13use script_bindings::root::DomRoot;
14use style::values::computed::Overflow;
15use style_traits::CSSPixel;
16use webrender_api::units::{LayoutSize, LayoutVector2D};
17
18use crate::dom::bindings::codegen::Bindings::ElementBinding::ScrollLogicalPosition;
19use crate::dom::node::{Node, NodeTraits};
20use crate::dom::types::{Document, Element};
21
22pub(crate) struct ScrollingBox {
23    target: ScrollingBoxSource,
24    overflow: AxesOverflow,
25    cached_content_size: Cell<Option<LayoutSize>>,
26    cached_size: Cell<Option<LayoutSize>>,
27}
28
29/// Represents a scrolling box that can be either an element or the viewport
30/// <https://drafts.csswg.org/cssom-view/#scrolling-box>
31pub(crate) enum ScrollingBoxSource {
32    Element(DomRoot<Element>),
33    Viewport(DomRoot<Document>),
34}
35
36#[derive(Copy, Clone)]
37pub(crate) enum ScrollingBoxAxis {
38    X,
39    Y,
40}
41
42#[derive(Copy, Clone)]
43pub(crate) enum ScrollRequirement {
44    Always,
45    IfNotVisible,
46}
47
48impl ScrollRequirement {
49    fn compute_need_scroll(
50        &self,
51        element_start: f32,
52        element_end: f32,
53        container_size: f32,
54    ) -> bool {
55        match self {
56            ScrollRequirement::Always => true,
57            ScrollRequirement::IfNotVisible => {
58                let scrollport_start = 0.;
59                let scrollport_end = container_size;
60
61                element_end <= scrollport_start || element_start >= scrollport_end
62            },
63        }
64    }
65}
66
67#[derive(Copy, Clone)]
68pub(crate) struct ScrollAxisState {
69    pub(crate) position: ScrollLogicalPosition,
70    pub(crate) requirement: ScrollRequirement,
71}
72
73impl ScrollAxisState {
74    pub fn new_always_scroll_position(position: ScrollLogicalPosition) -> Self {
75        ScrollAxisState {
76            position,
77            requirement: ScrollRequirement::Always,
78        }
79    }
80}
81
82impl ScrollingBox {
83    pub(crate) fn new(target: ScrollingBoxSource, overflow: AxesOverflow) -> Self {
84        Self {
85            target,
86            overflow,
87            cached_content_size: Default::default(),
88            cached_size: Default::default(),
89        }
90    }
91
92    pub(crate) fn target(&self) -> &ScrollingBoxSource {
93        &self.target
94    }
95
96    pub(crate) fn is_viewport(&self) -> bool {
97        matches!(self.target, ScrollingBoxSource::Viewport(..))
98    }
99
100    pub(crate) fn scroll_position(&self) -> LayoutVector2D {
101        match &self.target {
102            ScrollingBoxSource::Element(element) => element
103                .owner_window()
104                .scroll_offset_query(element.upcast::<Node>()),
105            ScrollingBoxSource::Viewport(document) => document.window().scroll_offset(),
106        }
107    }
108
109    pub(crate) fn content_size(&self) -> LayoutSize {
110        if let Some(content_size) = self.cached_content_size.get() {
111            return content_size;
112        }
113
114        let (document, node_to_query) = match &self.target {
115            ScrollingBoxSource::Element(element) => {
116                (element.owner_document(), Some(element.upcast()))
117            },
118            ScrollingBoxSource::Viewport(document) => (document.clone(), None),
119        };
120
121        let content_size = document
122            .window()
123            .scrolling_area_query(node_to_query)
124            .size
125            .to_f32()
126            .cast_unit();
127        self.cached_content_size.set(Some(content_size));
128        content_size
129    }
130
131    pub(crate) fn size(&self) -> LayoutSize {
132        if let Some(size) = self.cached_size.get() {
133            return size;
134        }
135
136        let size = match &self.target {
137            ScrollingBoxSource::Element(element) => element.client_rect().size.to_f32().cast_unit(),
138            ScrollingBoxSource::Viewport(document) => {
139                document.window().viewport_details().size.cast_unit()
140            },
141        };
142        self.cached_size.set(Some(size));
143        size
144    }
145
146    pub(crate) fn parent(&self) -> Option<ScrollingBox> {
147        match &self.target {
148            ScrollingBoxSource::Element(element) => {
149                element.scrolling_box(ScrollContainerQueryFlags::empty())
150            },
151            ScrollingBoxSource::Viewport(_) => None,
152        }
153    }
154
155    pub(crate) fn node(&self) -> &Node {
156        match &self.target {
157            ScrollingBoxSource::Element(element) => element.upcast(),
158            ScrollingBoxSource::Viewport(document) => document.upcast(),
159        }
160    }
161
162    pub(crate) fn scroll_to(
163        &self,
164        cx: &mut JSContext,
165        position: LayoutVector2D,
166        behavior: ScrollBehavior,
167    ) {
168        match &self.target {
169            ScrollingBoxSource::Element(element) => {
170                element
171                    .owner_window()
172                    .scroll_an_element(cx, element, position.x, position.y, behavior);
173            },
174            ScrollingBoxSource::Viewport(document) => {
175                document
176                    .window()
177                    .scroll(cx, position.x, position.y, behavior);
178            },
179        }
180    }
181
182    pub(crate) fn can_keyboard_scroll_in_axis(&self, axis: ScrollingBoxAxis) -> bool {
183        let overflow = match axis {
184            ScrollingBoxAxis::X => self.overflow.x,
185            ScrollingBoxAxis::Y => self.overflow.y,
186        };
187        if overflow == Overflow::Hidden {
188            return false;
189        }
190        match axis {
191            ScrollingBoxAxis::X => self.content_size().width > self.size().width,
192            ScrollingBoxAxis::Y => self.content_size().height > self.size().height,
193        }
194    }
195
196    /// <https://drafts.csswg.org/cssom-view/#determine-the-scroll-into-view-position>
197    pub(crate) fn determine_scroll_into_view_position(
198        &self,
199        block: ScrollAxisState,
200        inline: ScrollAxisState,
201        target_rect: Rect<Au, CSSPixel>,
202    ) -> LayoutVector2D {
203        let device_pixel_ratio = self.node().owner_window().device_pixel_ratio().get();
204        let to_pixel = |value: Au| value.to_nearest_pixel(device_pixel_ratio);
205
206        // Step 1 should be handled by the caller, and provided as |target_rect|.
207        // > Let target bounding border box be the box represented by the return value
208        // > of invoking Element’s getBoundingClientRect(), if target is an Element,
209        // > or Range’s getBoundingClientRect(), if target is a Range.
210        let target_top_left = target_rect.origin.map(to_pixel);
211        let target_bottom_right = target_rect.max().map(to_pixel);
212
213        // The rest of the steps diverge from the specification here, but essentially try
214        // to follow it using our own geometry types.
215        //
216        // TODO: This makes the code below wrong for the purposes of writing modes.
217        let (adjusted_element_top_left, adjusted_element_bottom_right) = match self.target() {
218            ScrollingBoxSource::Viewport(_) => (target_top_left, target_bottom_right),
219            ScrollingBoxSource::Element(scrolling_element) => {
220                let scrolling_padding_rect_top_left = scrolling_element
221                    .upcast::<Node>()
222                    .padding_box()
223                    .unwrap_or_default()
224                    .origin
225                    .map(to_pixel);
226                (
227                    target_top_left - scrolling_padding_rect_top_left.to_vector(),
228                    target_bottom_right - scrolling_padding_rect_top_left.to_vector(),
229                )
230            },
231        };
232
233        let size = self.size();
234        let current_scroll_position = self.scroll_position();
235        Vector2D::new(
236            Self::calculate_scroll_position_one_axis(
237                inline,
238                adjusted_element_top_left.x,
239                adjusted_element_bottom_right.x,
240                size.width,
241                current_scroll_position.x,
242            ),
243            Self::calculate_scroll_position_one_axis(
244                block,
245                adjusted_element_top_left.y,
246                adjusted_element_bottom_right.y,
247                size.height,
248                current_scroll_position.y,
249            ),
250        )
251    }
252
253    /// Step 10 from <https://drafts.csswg.org/cssom-view/#determine-the-scroll-into-view-position>:
254    // TODO: we are not considering the coordinate system of the element while deciding the scroll position.
255    fn calculate_scroll_position_one_axis(
256        state: ScrollAxisState,
257        element_start: f32,
258        element_end: f32,
259        container_size: f32,
260        current_scroll_offset: f32,
261    ) -> f32 {
262        if !state
263            .requirement
264            .compute_need_scroll(element_start, element_end, container_size)
265        {
266            return current_scroll_offset;
267        }
268
269        let element_size = element_end - element_start;
270
271        current_scroll_offset +
272            match state.position {
273                // Step 1 & 5: If inline is "start", then align element start edge with scrolling box start edge.
274                ScrollLogicalPosition::Start => element_start,
275                // Step 2 & 6: If inline is "end", then align element end edge with
276                // scrolling box end edge.
277                ScrollLogicalPosition::End => element_end - container_size,
278                // Step 3 & 7: If inline is "center", then align the center of target bounding
279                // border box with the center of scrolling box in scrolling box’s inline base direction.
280                ScrollLogicalPosition::Center => {
281                    element_start + (element_size - container_size) / 2.0
282                },
283                // Step 4 & 8: If inline is "nearest",
284                ScrollLogicalPosition::Nearest => {
285                    let scrollport_start = 0.;
286                    let scrollport_end = container_size;
287
288                    // Step 4.2 & 8.2: If element start edge is outside scrolling box start edge and element
289                    // size is less than scrolling box size or If element end edge is outside
290                    // scrolling box end edge and element size is greater than scrolling box size:
291                    // Align element start edge with scrolling box start edge.
292                    if (element_start < scrollport_start && element_size <= container_size) ||
293                        (element_end > scrollport_end && element_size >= container_size)
294                    {
295                        element_start
296                    }
297                    // Step 4.3 & 8.3: If element end edge is outside scrolling box start edge and element
298                    // size is greater than scrolling box size or If element start edge is outside
299                    // scrolling box end edge and element size is less than scrolling box size:
300                    // Align element end edge with scrolling box end edge.
301                    else if (element_end > scrollport_end && element_size < container_size) ||
302                        (element_start < scrollport_start && element_size > container_size)
303                    {
304                        element_end - container_size
305                    }
306                    // Step 4.1 & 8.1: If element start edge and element end edge are both outside scrolling
307                    // box start edge and scrolling box end edge or an invalid situation: Do nothing.
308                    else {
309                        0.
310                    }
311                },
312            }
313    }
314}