Skip to main content

script/dom/
visualviewport.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 bitflags::bitflags;
8use dom_struct::dom_struct;
9use euclid::{Rect, Scale, Size2D};
10use paint_api::PinchZoomInfos;
11use script_bindings::codegen::GenericBindings::VisualViewportBinding::VisualViewportMethods;
12use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
13use script_bindings::inheritance::Castable;
14use script_bindings::num::Finite;
15use script_bindings::reflector::reflect_dom_object;
16use script_bindings::root::{Dom, DomRoot};
17use script_bindings::script_runtime::CanGc;
18use style_traits::CSSPixel;
19use webrender_api::units::DevicePixel;
20
21use crate::dom::eventtarget::EventTarget;
22use crate::dom::window::Window;
23
24/// <https://drafts.csswg.org/cssom-view/#the-visualviewport-interface>
25#[dom_struct]
26pub(crate) struct VisualViewport {
27    eventtarget: EventTarget,
28
29    /// The associated [`Window`] for this [`VisualViewport`],
30    window: Dom<Window>,
31
32    /// The rectangle bound of the [`VisualViewport`], relative to the layout viewport.
33    #[no_trace]
34    viewport_rect: Cell<Rect<f32, CSSPixel>>,
35
36    /// The scale factor of [`VisualViewport`], which is also commonly known as pinch-zoom.
37    /// <https://drafts.csswg.org/cssom-view/#scale-factor>
38    #[no_trace]
39    scale: Cell<Scale<f32, DevicePixel, DevicePixel>>,
40}
41
42bitflags! {
43    /// The changes of the [`VisualViewport`] after an update to determine events that might need to be fired.
44    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
45    pub struct VisualViewportChanges: u8 {
46        /// Size or the scale factor of the [`VisualViewport`] has changed.
47        const DimensionChanged = 1 << 0;
48        /// The offset or the origin of the [`VisualViewport`] has changed.
49        const OffsetChanged = 1 << 1;
50    }
51}
52
53impl VisualViewport {
54    fn new_inherited(
55        window: &Window,
56        viewport_rect: Rect<f32, CSSPixel>,
57        scale: Scale<f32, DevicePixel, DevicePixel>,
58    ) -> Self {
59        Self {
60            eventtarget: EventTarget::new_inherited(),
61            window: Dom::from_ref(window),
62            viewport_rect: Cell::new(viewport_rect),
63            scale: Cell::new(scale),
64        }
65    }
66
67    /// The initial visual viewport based on a layout viewport relative to the initial containing block, where
68    /// the dimension would be the same as layout viewport leaving the offset and the scale to its default value.
69    pub(crate) fn new_from_layout_viewport(
70        window: &Window,
71        viewport_size: Size2D<f32, CSSPixel>,
72        can_gc: CanGc,
73    ) -> DomRoot<Self> {
74        reflect_dom_object(
75            Box::new(Self::new_inherited(
76                window,
77                Rect::from_size(viewport_size),
78                Scale::identity(),
79            )),
80            window,
81            can_gc,
82        )
83    }
84
85    /// Update just the scale of the [`VisualViewport`] without modifying the rect.
86    /// Used to eagerly apply the initial-scale from a viewport meta tag
87    /// before the paint thread round-trip completes.
88    pub(crate) fn update_scale(&self, scale: f32) {
89        self.scale.set(Scale::new(scale));
90    }
91
92    fn check_for_update(
93        &self,
94        old_rect: Rect<f32, CSSPixel>,
95        old_scale: Scale<f32, DevicePixel, DevicePixel>,
96    ) -> VisualViewportChanges {
97        let mut state = VisualViewportChanges::empty();
98        if old_rect.origin != self.viewport_rect.get().origin {
99            state |= VisualViewportChanges::OffsetChanged;
100        }
101        if old_rect.size != self.viewport_rect.get().size || old_scale != self.scale.get() {
102            state |= VisualViewportChanges::DimensionChanged;
103        }
104        state
105    }
106
107    /// Update the [`VisualViewport`] state based on a new [`PinchZoomInfos`].
108    pub(crate) fn update_from_pinch_zoom_infos(
109        &self,
110        pinch_zoom_infos: PinchZoomInfos,
111    ) -> VisualViewportChanges {
112        let old_rect = self.viewport_rect.replace(pinch_zoom_infos.rect);
113        let old_scale = self.scale.replace(pinch_zoom_infos.zoom_factor);
114
115        self.check_for_update(old_rect, old_scale)
116    }
117
118    /// <https://drafts.csswg.org/cssom-view/#scrolling-events>
119    ///
120    /// > Whenever a visual viewport gets scrolled (whether in response to user
121    /// > interaction or by an API), the user agent must run these steps:
122    pub(crate) fn handle_scroll_event(&self) {
123        // Step 1:  Let vv be the VisualViewport object that was scrolled.
124        // Note: This is self.
125        // Step 2: Let doc be vv’s associated document.
126        // Steps 3 and 4 are shared with other scroll targets.
127        self.window
128            .Document()
129            .finish_handle_scroll_event(self.upcast());
130    }
131}
132
133impl VisualViewportMethods<crate::DomTypeHolder> for VisualViewport {
134    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-offsetleft>
135    fn OffsetLeft(&self) -> Finite<f64> {
136        // > 1. If the visual viewport’s associated document is not fully active, return 0.
137        if !self.window.Document().is_fully_active() {
138            return Finite::wrap(0.);
139        }
140
141        // > 2. Otherwise, return the offset of the left edge of the visual viewport from the left edge of the
142        // >    layout viewport.
143        Finite::wrap(self.viewport_rect.get().min_x() as f64)
144    }
145
146    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-offsettop>
147    fn OffsetTop(&self) -> Finite<f64> {
148        // > 1. If the visual viewport’s associated document is not fully active, return 0.
149        if !self.window.Document().is_fully_active() {
150            return Finite::wrap(0.);
151        }
152
153        // > 2. Otherwise, return the offset of the top edge of the visual viewport from the top edge of the
154        // >    layout viewport.
155        Finite::wrap(self.viewport_rect.get().min_y() as f64)
156    }
157
158    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-pageleft>
159    fn PageLeft(&self) -> Finite<f64> {
160        // > 1. If the visual viewport’s associated document is not fully active, return 0.
161        if !self.window.Document().is_fully_active() {
162            return Finite::wrap(0.);
163        }
164
165        // > 2. Otherwise, return the offset of the left edge of the visual viewport from the left edge of the
166        // >    initial containing block of the layout viewport’s document.
167        let page_left = self.viewport_rect.get().min_x() + self.window.scroll_offset().x;
168        Finite::wrap(page_left as f64)
169    }
170
171    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-pagetop>
172    fn PageTop(&self) -> Finite<f64> {
173        // > 1. If the visual viewport’s associated document is not fully active, return 0.
174        if !self.window.Document().is_fully_active() {
175            return Finite::wrap(0.);
176        }
177
178        // > 2. Otherwise, return the offset of the top edge of the visual viewport from the top edge of the
179        // >    initial containing block of the layout viewport’s document.
180        let page_top = self.viewport_rect.get().min_y() + self.window.scroll_offset().y;
181        Finite::wrap(page_top as f64)
182    }
183
184    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-width>
185    fn Width(&self) -> Finite<f64> {
186        // > 1. If the visual viewport’s associated document is not fully active, return 0.
187        if !self.window.Document().is_fully_active() {
188            return Finite::wrap(0.);
189        }
190
191        // > 2. Otherwise, return the width of the visual viewport excluding the width of any rendered vertical
192        // >    classic scrollbar that is fixed to the visual viewport.
193        // TODO(#41341): when classic scrollbar is implemented, exclude it's size from visual viewport width.
194        Finite::wrap(self.viewport_rect.get().width() as f64)
195    }
196
197    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-height>
198    fn Height(&self) -> Finite<f64> {
199        // > 1. If the visual viewport’s associated document is not fully active, return 0.
200        if !self.window.Document().is_fully_active() {
201            return Finite::wrap(0.);
202        }
203
204        // > 2. Otherwise, return the height of the visual viewport excluding the height of any rendered horizontal
205        // >    classic scrollbar that is fixed to the visual viewport.
206        // TODO(#41341): when classic scrollbar is implemented, exclude it's size from visual viewport height.
207        Finite::wrap(self.viewport_rect.get().height() as f64)
208    }
209
210    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-scale>
211    fn Scale(&self) -> Finite<f64> {
212        // > 1. If the visual viewport’s associated document is not fully active, return 0.
213        if !self.window.Document().is_fully_active() {
214            return Finite::wrap(0.);
215        }
216
217        // > 2. If there is no output device, return 1 and abort these steps.
218        // TODO(#41341): check for output device.
219
220        // > 3. Otherwise, return the visual viewport’s scale factor.
221        Finite::wrap(self.scale.get().get() as f64)
222    }
223
224    // <https://drafts.csswg.org/cssom-view/#dom-visualviewport-onresize>
225    event_handler!(resize, GetOnresize, SetOnresize);
226
227    // <https://drafts.csswg.org/cssom-view/#dom-visualviewport-onscroll>
228    event_handler!(scroll, GetOnscroll, SetOnscroll);
229
230    // <https://drafts.csswg.org/cssom-view/#dom-visualviewport-onscrollend>
231    event_handler!(scrollend, GetOnscrollend, SetOnscrollend);
232}