use std::cell::Cell;
use app_units::Au;
use euclid::{Rect, Vector2D};
use layout_api::{AxesOverflow, ScrollContainerQueryFlags};
use script_bindings::codegen::GenericBindings::WindowBinding::ScrollBehavior;
use script_bindings::inheritance::Castable;
use script_bindings::root::DomRoot;
use style::values::computed::Overflow;
use style_traits::CSSPixel;
use webrender_api::units::{LayoutSize, LayoutVector2D};
use crate::dom::bindings::codegen::Bindings::ElementBinding::ScrollLogicalPosition;
use crate::dom::node::{Node, NodeTraits};
use crate::dom::types::{Document, Element};
pub(crate) struct ScrollingBox {
target: ScrollingBoxSource,
overflow: AxesOverflow,
cached_content_size: Cell<Option<LayoutSize>>,
cached_size: Cell<Option<LayoutSize>>,
}
pub(crate) enum ScrollingBoxSource {
Element(DomRoot<Element>),
Viewport(DomRoot<Document>),
}
#[derive(Copy, Clone)]
pub(crate) enum ScrollingBoxAxis {
X,
Y,
}
#[derive(Copy, Clone)]
pub(crate) enum ScrollRequirement {
Always,
IfNotVisible,
}
impl ScrollRequirement {
fn compute_need_scroll(
&self,
element_start: f32,
element_end: f32,
container_size: f32,
) -> bool {
match self {
ScrollRequirement::Always => true,
ScrollRequirement::IfNotVisible => {
let scrollport_start = 0.;
let scrollport_end = container_size;
element_end <= scrollport_start || element_start >= scrollport_end
},
}
}
}
#[derive(Copy, Clone)]
pub(crate) struct ScrollAxisState {
pub(crate) position: ScrollLogicalPosition,
pub(crate) requirement: ScrollRequirement,
}
impl ScrollAxisState {
pub fn new_always_scroll_position(position: ScrollLogicalPosition) -> Self {
ScrollAxisState {
position,
requirement: ScrollRequirement::Always,
}
}
}
impl ScrollingBox {
pub(crate) fn new(target: ScrollingBoxSource, overflow: AxesOverflow) -> Self {
Self {
target,
overflow,
cached_content_size: Default::default(),
cached_size: Default::default(),
}
}
pub(crate) fn target(&self) -> &ScrollingBoxSource {
&self.target
}
pub(crate) fn is_viewport(&self) -> bool {
matches!(self.target, ScrollingBoxSource::Viewport(..))
}
pub(crate) fn scroll_position(&self) -> LayoutVector2D {
match &self.target {
ScrollingBoxSource::Element(element) => element
.owner_window()
.scroll_offset_query(element.upcast::<Node>()),
ScrollingBoxSource::Viewport(document) => document.window().scroll_offset(),
}
}
pub(crate) fn content_size(&self) -> LayoutSize {
if let Some(content_size) = self.cached_content_size.get() {
return content_size;
}
let (document, node_to_query) = match &self.target {
ScrollingBoxSource::Element(element) => {
(element.owner_document(), Some(element.upcast()))
},
ScrollingBoxSource::Viewport(document) => (document.clone(), None),
};
let content_size = document
.window()
.scrolling_area_query(node_to_query)
.size
.to_f32()
.cast_unit();
self.cached_content_size.set(Some(content_size));
content_size
}
pub(crate) fn size(&self) -> LayoutSize {
if let Some(size) = self.cached_size.get() {
return size;
}
let size = match &self.target {
ScrollingBoxSource::Element(element) => element.client_rect().size.to_f32().cast_unit(),
ScrollingBoxSource::Viewport(document) => {
document.window().viewport_details().size.cast_unit()
},
};
self.cached_size.set(Some(size));
size
}
pub(crate) fn parent(&self) -> Option<ScrollingBox> {
match &self.target {
ScrollingBoxSource::Element(element) => {
element.scrolling_box(ScrollContainerQueryFlags::empty())
},
ScrollingBoxSource::Viewport(_) => None,
}
}
pub(crate) fn node(&self) -> &Node {
match &self.target {
ScrollingBoxSource::Element(element) => element.upcast(),
ScrollingBoxSource::Viewport(document) => document.upcast(),
}
}
pub(crate) fn scroll_to(&self, position: LayoutVector2D, behavior: ScrollBehavior) {
match &self.target {
ScrollingBoxSource::Element(element) => {
element
.owner_window()
.scroll_an_element(element, position.x, position.y, behavior);
},
ScrollingBoxSource::Viewport(document) => {
document.window().scroll(position.x, position.y, behavior);
},
}
}
pub(crate) fn can_keyboard_scroll_in_axis(&self, axis: ScrollingBoxAxis) -> bool {
let overflow = match axis {
ScrollingBoxAxis::X => self.overflow.x,
ScrollingBoxAxis::Y => self.overflow.y,
};
if overflow == Overflow::Hidden {
return false;
}
match axis {
ScrollingBoxAxis::X => self.content_size().width > self.size().width,
ScrollingBoxAxis::Y => self.content_size().height > self.size().height,
}
}
pub(crate) fn determine_scroll_into_view_position(
&self,
block: ScrollAxisState,
inline: ScrollAxisState,
target_rect: Rect<Au, CSSPixel>,
) -> LayoutVector2D {
let device_pixel_ratio = self.node().owner_window().device_pixel_ratio().get();
let to_pixel = |value: Au| value.to_nearest_pixel(device_pixel_ratio);
let target_top_left = target_rect.origin.map(to_pixel);
let target_bottom_right = target_rect.max().map(to_pixel);
let (adjusted_element_top_left, adjusted_element_bottom_right) = match self.target() {
ScrollingBoxSource::Viewport(_) => (target_top_left, target_bottom_right),
ScrollingBoxSource::Element(scrolling_element) => {
let scrolling_padding_rect_top_left = scrolling_element
.upcast::<Node>()
.padding_box()
.unwrap_or_default()
.origin
.map(to_pixel);
(
target_top_left - scrolling_padding_rect_top_left.to_vector(),
target_bottom_right - scrolling_padding_rect_top_left.to_vector(),
)
},
};
let size = self.size();
let current_scroll_position = self.scroll_position();
Vector2D::new(
Self::calculate_scroll_position_one_axis(
inline,
adjusted_element_top_left.x,
adjusted_element_bottom_right.x,
size.width,
current_scroll_position.x,
),
Self::calculate_scroll_position_one_axis(
block,
adjusted_element_top_left.y,
adjusted_element_bottom_right.y,
size.height,
current_scroll_position.y,
),
)
}
fn calculate_scroll_position_one_axis(
state: ScrollAxisState,
element_start: f32,
element_end: f32,
container_size: f32,
current_scroll_offset: f32,
) -> f32 {
if !state
.requirement
.compute_need_scroll(element_start, element_end, container_size)
{
return current_scroll_offset;
}
let element_size = element_end - element_start;
current_scroll_offset +
match state.position {
ScrollLogicalPosition::Start => element_start,
ScrollLogicalPosition::End => element_end - container_size,
ScrollLogicalPosition::Center => {
element_start + (element_size - container_size) / 2.0
},
ScrollLogicalPosition::Nearest => {
let scrollport_start = 0.;
let scrollport_end = container_size;
if (element_start < scrollport_start && element_size <= container_size) ||
(element_end > scrollport_end && element_size >= container_size)
{
element_start
}
else if (element_end > scrollport_end && element_size < container_size) ||
(element_start < scrollport_start && element_size > container_size)
{
element_end - container_size
}
else {
0.
}
},
}
}
}