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