servo-script 0.1.0

A component of the servo web-engine.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use std::cell::Cell;

use bitflags::bitflags;
use dom_struct::dom_struct;
use euclid::{Rect, Scale, Size2D};
use paint_api::PinchZoomInfos;
use script_bindings::codegen::GenericBindings::VisualViewportBinding::VisualViewportMethods;
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
use script_bindings::inheritance::Castable;
use script_bindings::num::Finite;
use script_bindings::root::{Dom, DomRoot};
use script_bindings::script_runtime::CanGc;
use style_traits::CSSPixel;
use webrender_api::units::DevicePixel;

use crate::dom::bindings::reflector::reflect_dom_object;
use crate::dom::eventtarget::EventTarget;
use crate::dom::window::Window;

/// <https://drafts.csswg.org/cssom-view/#the-visualviewport-interface>
#[dom_struct]
pub(crate) struct VisualViewport {
    eventtarget: EventTarget,

    /// The associated [`Window`] for this [`VisualViewport`],
    window: Dom<Window>,

    /// The rectangle bound of the [`VisualViewport`], relative to the layout viewport.
    #[no_trace]
    viewport_rect: Cell<Rect<f32, CSSPixel>>,

    /// The scale factor of [`VisualViewport`], which is also commonly known as pinch-zoom.
    /// <https://drafts.csswg.org/cssom-view/#scale-factor>
    #[no_trace]
    scale: Cell<Scale<f32, DevicePixel, DevicePixel>>,
}

bitflags! {
    /// The changes of the [`VisualViewport`] after an update to determine events that might need to be fired.
    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    pub struct VisualViewportChanges: u8 {
        /// Size or the scale factor of the [`VisualViewport`] has changed.
        const DimensionChanged = 1 << 0;
        /// The offset or the origin of the [`VisualViewport`] has changed.
        const OffsetChanged = 1 << 1;
    }
}

impl VisualViewport {
    fn new_inherited(
        window: &Window,
        viewport_rect: Rect<f32, CSSPixel>,
        scale: Scale<f32, DevicePixel, DevicePixel>,
    ) -> Self {
        Self {
            eventtarget: EventTarget::new_inherited(),
            window: Dom::from_ref(window),
            viewport_rect: Cell::new(viewport_rect),
            scale: Cell::new(scale),
        }
    }

    /// The initial visual viewport based on a layout viewport relative to the initial containing block, where
    /// the dimension would be the same as layout viewport leaving the offset and the scale to its default value.
    pub(crate) fn new_from_layout_viewport(
        window: &Window,
        viewport_size: Size2D<f32, CSSPixel>,
        can_gc: CanGc,
    ) -> DomRoot<Self> {
        reflect_dom_object(
            Box::new(Self::new_inherited(
                window,
                Rect::from_size(viewport_size),
                Scale::identity(),
            )),
            window,
            can_gc,
        )
    }

    fn check_for_update(
        &self,
        old_rect: Rect<f32, CSSPixel>,
        old_scale: Scale<f32, DevicePixel, DevicePixel>,
    ) -> VisualViewportChanges {
        let mut state = VisualViewportChanges::empty();
        if old_rect.origin != self.viewport_rect.get().origin {
            state |= VisualViewportChanges::OffsetChanged;
        }
        if old_rect.size != self.viewport_rect.get().size || old_scale != self.scale.get() {
            state |= VisualViewportChanges::DimensionChanged;
        }
        state
    }

    /// Update the [`VisualViewport`] state based on a new [`PinchZoomInfos`].
    pub(crate) fn update_from_pinch_zoom_infos(
        &self,
        pinch_zoom_infos: PinchZoomInfos,
    ) -> VisualViewportChanges {
        let old_rect = self.viewport_rect.replace(pinch_zoom_infos.rect);
        let old_scale = self.scale.replace(pinch_zoom_infos.zoom_factor);

        self.check_for_update(old_rect, old_scale)
    }

    /// <https://drafts.csswg.org/cssom-view/#scrolling-events>
    ///
    /// > Whenever a visual viewport gets scrolled (whether in response to user
    /// > interaction or by an API), the user agent must run these steps:
    pub(crate) fn handle_scroll_event(&self) {
        // Step 1:  Let vv be the VisualViewport object that was scrolled.
        // Note: This is self.
        // Step 2: Let doc be vv’s associated document.
        // Steps 3 and 4 are shared with other scroll targets.
        self.window
            .Document()
            .finish_handle_scroll_event(self.upcast());
    }
}

impl VisualViewportMethods<crate::DomTypeHolder> for VisualViewport {
    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-offsetleft>
    fn OffsetLeft(&self) -> Finite<f64> {
        // > 1. If the visual viewport’s associated document is not fully active, return 0.
        if !self.window.Document().is_fully_active() {
            return Finite::wrap(0.);
        }

        // > 2. Otherwise, return the offset of the left edge of the visual viewport from the left edge of the
        // >    layout viewport.
        Finite::wrap(self.viewport_rect.get().min_x() as f64)
    }

    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-offsettop>
    fn OffsetTop(&self) -> Finite<f64> {
        // > 1. If the visual viewport’s associated document is not fully active, return 0.
        if !self.window.Document().is_fully_active() {
            return Finite::wrap(0.);
        }

        // > 2. Otherwise, return the offset of the top edge of the visual viewport from the top edge of the
        // >    layout viewport.
        Finite::wrap(self.viewport_rect.get().min_y() as f64)
    }

    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-pageleft>
    fn PageLeft(&self) -> Finite<f64> {
        // > 1. If the visual viewport’s associated document is not fully active, return 0.
        if !self.window.Document().is_fully_active() {
            return Finite::wrap(0.);
        }

        // > 2. Otherwise, return the offset of the left edge of the visual viewport from the left edge of the
        // >    initial containing block of the layout viewport’s document.
        let page_left = self.viewport_rect.get().min_x() + self.window.scroll_offset().x;
        Finite::wrap(page_left as f64)
    }

    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-pagetop>
    fn PageTop(&self) -> Finite<f64> {
        // > 1. If the visual viewport’s associated document is not fully active, return 0.
        if !self.window.Document().is_fully_active() {
            return Finite::wrap(0.);
        }

        // > 2. Otherwise, return the offset of the top edge of the visual viewport from the top edge of the
        // >    initial containing block of the layout viewport’s document.
        let page_top = self.viewport_rect.get().min_y() + self.window.scroll_offset().y;
        Finite::wrap(page_top as f64)
    }

    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-width>
    fn Width(&self) -> Finite<f64> {
        // > 1. If the visual viewport’s associated document is not fully active, return 0.
        if !self.window.Document().is_fully_active() {
            return Finite::wrap(0.);
        }

        // > 2. Otherwise, return the width of the visual viewport excluding the width of any rendered vertical
        // >    classic scrollbar that is fixed to the visual viewport.
        // TODO(#41341): when classic scrollbar is implemented, exclude it's size from visual viewport width.
        Finite::wrap(self.viewport_rect.get().width() as f64)
    }

    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-height>
    fn Height(&self) -> Finite<f64> {
        // > 1. If the visual viewport’s associated document is not fully active, return 0.
        if !self.window.Document().is_fully_active() {
            return Finite::wrap(0.);
        }

        // > 2. Otherwise, return the height of the visual viewport excluding the height of any rendered horizontal
        // >    classic scrollbar that is fixed to the visual viewport.
        // TODO(#41341): when classic scrollbar is implemented, exclude it's size from visual viewport height.
        Finite::wrap(self.viewport_rect.get().height() as f64)
    }

    /// <https://drafts.csswg.org/cssom-view/#dom-visualviewport-scale>
    fn Scale(&self) -> Finite<f64> {
        // > 1. If the visual viewport’s associated document is not fully active, return 0.
        if !self.window.Document().is_fully_active() {
            return Finite::wrap(0.);
        }

        // > 2. If there is no output device, return 1 and abort these steps.
        // TODO(#41341): check for output device.

        // > 3. Otherwise, return the visual viewport’s scale factor.
        Finite::wrap(self.scale.get().get() as f64)
    }

    // <https://drafts.csswg.org/cssom-view/#dom-visualviewport-onresize>
    event_handler!(resize, GetOnresize, SetOnresize);

    // <https://drafts.csswg.org/cssom-view/#dom-visualviewport-onscroll>
    event_handler!(scroll, GetOnscroll, SetOnscroll);

    // <https://drafts.csswg.org/cssom-view/#dom-visualviewport-onscrollend>
    event_handler!(scrollend, GetOnscrollend, SetOnscrollend);
}