script/dom/
scrolling_box.rs1use 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
28pub(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 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 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 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 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 ScrollLogicalPosition::Start => element_start,
218 ScrollLogicalPosition::End => element_end - container_size,
221 ScrollLogicalPosition::Center => {
224 element_start + (element_size - container_size) / 2.0
225 },
226 ScrollLogicalPosition::Nearest => {
228 let viewport_start = current_scroll_offset;
229 let viewport_end = current_scroll_offset + container_size;
230
231 if (element_start < viewport_start && element_size <= container_size) ||
236 (element_end > viewport_end && element_size >= container_size)
237 {
238 element_start
239 }
240 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 else {
252 current_scroll_offset
253 }
254 },
255 }
256 }
257}