Skip to main content

script/dom/window/
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, NoGC};
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, no_gc: &NoGC) -> 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) => {
138                element.client_rect(no_gc).size.to_f32().cast_unit()
139            },
140            ScrollingBoxSource::Viewport(document) => {
141                document.window().viewport_details().size.cast_unit()
142            },
143        };
144        self.cached_size.set(Some(size));
145        size
146    }
147
148    pub(crate) fn parent(&self) -> Option<ScrollingBox> {
149        match &self.target {
150            ScrollingBoxSource::Element(element) => {
151                element.scrolling_box(ScrollContainerQueryFlags::empty())
152            },
153            ScrollingBoxSource::Viewport(_) => None,
154        }
155    }
156
157    pub(crate) fn node(&self) -> &Node {
158        match &self.target {
159            ScrollingBoxSource::Element(element) => element.upcast(),
160            ScrollingBoxSource::Viewport(document) => document.upcast(),
161        }
162    }
163
164    pub(crate) fn scroll_to(
165        &self,
166        cx: &mut JSContext,
167        position: LayoutVector2D,
168        behavior: ScrollBehavior,
169    ) {
170        match &self.target {
171            ScrollingBoxSource::Element(element) => {
172                element
173                    .owner_window()
174                    .scroll_an_element(cx, element, position.x, position.y, behavior);
175            },
176            ScrollingBoxSource::Viewport(document) => {
177                document
178                    .window()
179                    .scroll(cx, position.x, position.y, behavior);
180            },
181        }
182    }
183
184    pub(crate) fn can_keyboard_scroll_in_axis(&self, no_gc: &NoGC, axis: ScrollingBoxAxis) -> bool {
185        let overflow = match axis {
186            ScrollingBoxAxis::X => self.overflow.x,
187            ScrollingBoxAxis::Y => self.overflow.y,
188        };
189        if overflow == Overflow::Hidden {
190            return false;
191        }
192        match axis {
193            ScrollingBoxAxis::X => self.content_size().width > self.size(no_gc).width,
194            ScrollingBoxAxis::Y => self.content_size().height > self.size(no_gc).height,
195        }
196    }
197
198    /// <https://drafts.csswg.org/cssom-view/#determine-the-scroll-into-view-position>
199    pub(crate) fn determine_scroll_into_view_position(
200        &self,
201        no_gc: &NoGC,
202        block: ScrollAxisState,
203        inline: ScrollAxisState,
204        target_rect: Rect<Au, CSSPixel>,
205    ) -> LayoutVector2D {
206        let device_pixel_ratio = self.node().owner_window().device_pixel_ratio().get();
207        let to_pixel = |value: Au| value.to_nearest_pixel(device_pixel_ratio);
208
209        // Step 1 should be handled by the caller, and provided as |target_rect|.
210        // > Let target bounding border box be the box represented by the return value
211        // > of invoking Element’s getBoundingClientRect(), if target is an Element,
212        // > or Range’s getBoundingClientRect(), if target is a Range.
213        let target_top_left = target_rect.origin.map(to_pixel);
214        let target_bottom_right = target_rect.max().map(to_pixel);
215
216        // The rest of the steps diverge from the specification here, but essentially try
217        // to follow it using our own geometry types.
218        //
219        // TODO: This makes the code below wrong for the purposes of writing modes.
220        let (adjusted_element_top_left, adjusted_element_bottom_right) = match self.target() {
221            ScrollingBoxSource::Viewport(_) => (target_top_left, target_bottom_right),
222            ScrollingBoxSource::Element(scrolling_element) => {
223                let scrolling_padding_rect_top_left = scrolling_element
224                    .upcast::<Node>()
225                    .padding_box()
226                    .unwrap_or_default()
227                    .origin
228                    .map(to_pixel);
229                (
230                    target_top_left - scrolling_padding_rect_top_left.to_vector(),
231                    target_bottom_right - scrolling_padding_rect_top_left.to_vector(),
232                )
233            },
234        };
235
236        let size = self.size(no_gc);
237        let current_scroll_position = self.scroll_position();
238        Vector2D::new(
239            Self::calculate_scroll_position_one_axis(
240                inline,
241                adjusted_element_top_left.x,
242                adjusted_element_bottom_right.x,
243                size.width,
244                current_scroll_position.x,
245            ),
246            Self::calculate_scroll_position_one_axis(
247                block,
248                adjusted_element_top_left.y,
249                adjusted_element_bottom_right.y,
250                size.height,
251                current_scroll_position.y,
252            ),
253        )
254    }
255
256    /// Step 10 from <https://drafts.csswg.org/cssom-view/#determine-the-scroll-into-view-position>:
257    // TODO: we are not considering the coordinate system of the element while deciding the scroll position.
258    fn calculate_scroll_position_one_axis(
259        state: ScrollAxisState,
260        element_start: f32,
261        element_end: f32,
262        container_size: f32,
263        current_scroll_offset: f32,
264    ) -> f32 {
265        if !state
266            .requirement
267            .compute_need_scroll(element_start, element_end, container_size)
268        {
269            return current_scroll_offset;
270        }
271
272        let element_size = element_end - element_start;
273
274        current_scroll_offset +
275            match state.position {
276                // Step 1 & 5: If inline is "start", then align element start edge with scrolling box start edge.
277                ScrollLogicalPosition::Start => element_start,
278                // Step 2 & 6: If inline is "end", then align element end edge with
279                // scrolling box end edge.
280                ScrollLogicalPosition::End => element_end - container_size,
281                // Step 3 & 7: If inline is "center", then align the center of target bounding
282                // border box with the center of scrolling box in scrolling box’s inline base direction.
283                ScrollLogicalPosition::Center => {
284                    element_start + (element_size - container_size) / 2.0
285                },
286                // Step 4 & 8: If inline is "nearest",
287                ScrollLogicalPosition::Nearest => {
288                    let scrollport_start = 0.;
289                    let scrollport_end = container_size;
290
291                    // Step 4.2 & 8.2: If element start edge is outside scrolling box start edge and element
292                    // size is less than scrolling box size or If element end edge is outside
293                    // scrolling box end edge and element size is greater than scrolling box size:
294                    // Align element start edge with scrolling box start edge.
295                    if (element_start < scrollport_start && element_size <= container_size) ||
296                        (element_end > scrollport_end && element_size >= container_size)
297                    {
298                        element_start
299                    }
300                    // Step 4.3 & 8.3: If element end edge is outside scrolling box start edge and element
301                    // size is greater than scrolling box size or If element start edge is outside
302                    // scrolling box end edge and element size is less than scrolling box size:
303                    // Align element end edge with scrolling box end edge.
304                    else if (element_end > scrollport_end && element_size < container_size) ||
305                        (element_start < scrollport_start && element_size > container_size)
306                    {
307                        element_end - container_size
308                    }
309                    // Step 4.1 & 8.1: If element start edge and element end edge are both outside scrolling
310                    // box start edge and scrolling box end edge or an invalid situation: Do nothing.
311                    else {
312                        0.
313                    }
314                },
315            }
316    }
317}