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