diff --git a/components/layout/flow/root.rs b/components/layout/flow/root.rs index 3921270a031..50e07a1f565 100644 --- a/components/layout/flow/root.rs +++ b/components/layout/flow/root.rs @@ -8,7 +8,7 @@ use compositing_traits::display_list::AxesScrollSensitivity; use euclid::Rect; use euclid::default::Size2D as UntypedSize2D; use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode}; -use layout_api::{LayoutElementType, LayoutNodeType}; +use layout_api::{AxesOverflow, LayoutElementType, LayoutNodeType}; use malloc_size_of_derive::MallocSizeOf; use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode}; use servo_arc::Arc; @@ -31,7 +31,7 @@ use crate::fragment_tree::{FragmentFlags, FragmentTree}; use crate::geom::{LogicalVec2, PhysicalSize}; use crate::positioned::{AbsolutelyPositionedBox, PositioningContext}; use crate::replaced::ReplacedContents; -use crate::style_ext::{AxesOverflow, Display, DisplayGeneratingBox, DisplayInside}; +use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside}; use crate::taffy::{TaffyItemBox, TaffyItemBoxInner}; use crate::{DefiniteContainingBlock, PropagatedBoxTreeData}; diff --git a/components/layout/layout_impl.rs b/components/layout/layout_impl.rs index 6c7c2bac3ec..f83470c4155 100644 --- a/components/layout/layout_impl.rs +++ b/components/layout/layout_impl.rs @@ -29,7 +29,7 @@ use layout_api::{ BoxAreaType, IFrameSizes, Layout, LayoutConfig, LayoutDamage, LayoutFactory, OffsetParentResponse, PropertyRegistration, QueryMsg, ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, ReflowResult, RegisterPropertyError, - ScrollContainerQueryType, ScrollContainerResponse, TrustedNodeAddress, + ScrollContainerQueryFlags, ScrollContainerResponse, TrustedNodeAddress, }; use log::{debug, error, warn}; use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps}; @@ -334,10 +334,10 @@ impl Layout for LayoutThread { fn query_scroll_container( &self, node: TrustedNodeAddress, - query_type: ScrollContainerQueryType, + flags: ScrollContainerQueryFlags, ) -> Option { let node = unsafe { ServoLayoutNode::new(&node) }; - process_scroll_container_query(node, query_type) + process_scroll_container_query(node, flags) } #[servo_tracing::instrument(skip_all)] diff --git a/components/layout/query.rs b/components/layout/query.rs index 4776e5f0fbe..e73344a7ad8 100644 --- a/components/layout/query.rs +++ b/components/layout/query.rs @@ -12,8 +12,8 @@ use euclid::{SideOffsets2D, Size2D}; use itertools::Itertools; use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode}; use layout_api::{ - BoxAreaType, LayoutElementType, LayoutNodeType, OffsetParentResponse, ScrollContainerQueryType, - ScrollContainerResponse, + BoxAreaType, LayoutElementType, LayoutNodeType, OffsetParentResponse, + ScrollContainerQueryFlags, ScrollContainerResponse, }; use script::layout_dom::{ServoLayoutNode, ServoThreadSafeLayoutNode}; use servo_arc::Arc as ServoArc; @@ -677,7 +677,7 @@ pub fn process_offset_parent_query( #[inline] pub(crate) fn process_scroll_container_query( node: ServoLayoutNode<'_>, - query_type: ScrollContainerQueryType, + query_flags: ScrollContainerQueryFlags, ) -> Option { let layout_data = node.to_threadsafe().inner_layout_data()?; @@ -686,15 +686,15 @@ pub(crate) fn process_scroll_container_query( let layout_box = layout_data.self_box.borrow(); let layout_box = layout_box.as_ref()?; - let (mut current_position_value, flags) = layout_box - .with_first_base(|base| (base.style.clone_position(), base.base_fragment_info.flags))?; + let (style, flags) = + layout_box.with_first_base(|base| (base.style.clone(), base.base_fragment_info.flags))?; // - The element is the root element. // - The element is the body element. // // Note: We only do this for `scrollParent`, which needs to be null. But `scrollIntoView` on the // `` or root element should still bring it into view by scrolling the viewport. - if query_type == ScrollContainerQueryType::ForScrollParent && + if query_flags.contains(ScrollContainerQueryFlags::ForScrollParent) && flags.intersects( FragmentFlags::IS_ROOT_ELEMENT | FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT, ) @@ -702,6 +702,15 @@ pub(crate) fn process_scroll_container_query( return None; } + if query_flags.contains(ScrollContainerQueryFlags::Inclusive) && + style.establishes_scroll_container(flags) + { + return Some(ScrollContainerResponse::Element( + node.opaque().into(), + style.effective_overflow(flags), + )); + } + // - The element’s computed value of the position property is fixed and no ancestor // establishes a fixed position containing block. // @@ -721,6 +730,7 @@ pub(crate) fn process_scroll_container_query( // Notes: We don't follow the specification exactly below, but we follow the spirit. // // TODO: Handle the situation where the ancestor is "closed-shadow-hidden" from the element. + let mut current_position_value = style.clone_position(); let mut current_ancestor = node.as_element()?; while let Some(ancestor) = current_ancestor.traversal_parent() { current_ancestor = ancestor; @@ -757,6 +767,7 @@ pub(crate) fn process_scroll_container_query( if ancestor_style.establishes_scroll_container(ancestor_flags) { return Some(ScrollContainerResponse::Element( ancestor.as_node().opaque().into(), + ancestor_style.effective_overflow(ancestor_flags), )); } diff --git a/components/layout/style_ext.rs b/components/layout/style_ext.rs index 5c2ea84000c..53ca0b87405 100644 --- a/components/layout/style_ext.rs +++ b/components/layout/style_ext.rs @@ -3,6 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use app_units::Au; +use layout_api::AxesOverflow; use malloc_size_of_derive::MallocSizeOf; use style::Zero; use style::color::AbsoluteColor; @@ -58,30 +59,6 @@ pub(crate) enum DisplayGeneratingBox { /// LayoutInternal(DisplayLayoutInternal), } -#[derive(Clone, Copy, Debug)] -pub struct AxesOverflow { - pub x: Overflow, - pub y: Overflow, -} - -impl Default for AxesOverflow { - fn default() -> Self { - Self { - x: Overflow::Visible, - y: Overflow::Visible, - } - } -} - -impl From<&ComputedValues> for AxesOverflow { - fn from(style: &ComputedValues) -> Self { - Self { - x: style.clone_overflow_x(), - y: style.clone_overflow_y(), - } - } -} - impl DisplayGeneratingBox { pub(crate) fn display_inside(&self) -> DisplayInside { match *self { diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index a9c7f946ced..b97e3b3e052 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -174,6 +174,7 @@ use crate::dom::processinginstruction::ProcessingInstruction; use crate::dom::promise::Promise; use crate::dom::range::Range; use crate::dom::resizeobserver::{ResizeObservationDepth, ResizeObserver}; +use crate::dom::scrolling_box::{ScrollingBox, ScrollingBoxSource}; use crate::dom::selection::Selection; use crate::dom::servoparser::ServoParser; use crate::dom::shadowroot::ShadowRoot; @@ -4447,6 +4448,10 @@ impl Document { pub(crate) fn set_active_sandboxing_flag_set(&self, flags: SandboxingFlagSet) { self.active_sandboxing_flag_set.set(flags) } + + pub(crate) fn viewport_scrolling_box(&self) -> ScrollingBox { + ScrollingBox::new(ScrollingBoxSource::Viewport(DomRoot::from_ref(self))) + } } #[allow(non_snake_case)] diff --git a/components/script/dom/document_event_handler.rs b/components/script/dom/document_event_handler.rs index 734a081a1fd..f87b58f78bb 100644 --- a/components/script/dom/document_event_handler.rs +++ b/components/script/dom/document_event_handler.rs @@ -18,17 +18,17 @@ use embedder_traits::{ MouseLeftViewportEvent, ScrollEvent, TouchEvent as EmbedderTouchEvent, TouchEventType, TouchId, UntrustedNodeAddress, WheelEvent as EmbedderWheelEvent, }; -use euclid::Point2D; +use euclid::{Point2D, Vector2D}; use ipc_channel::ipc; use keyboard_types::{Code, Key, KeyState, Modifiers, NamedKey}; -use layout_api::node_id_from_scroll_id; +use layout_api::{ScrollContainerQueryFlags, node_id_from_scroll_id}; use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; use script_bindings::codegen::GenericBindings::EventBinding::EventMethods; use script_bindings::codegen::GenericBindings::NavigatorBinding::NavigatorMethods; use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods; use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods; use script_bindings::codegen::GenericBindings::TouchBinding::TouchMethods; -use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods; +use script_bindings::codegen::GenericBindings::WindowBinding::{ScrollBehavior, WindowMethods}; use script_bindings::inheritance::Castable; use script_bindings::num::Finite; use script_bindings::root::{Dom, DomRoot, DomSlice}; @@ -50,6 +50,7 @@ use crate::dom::gamepad::gamepadevent::GamepadEventType; use crate::dom::inputevent::HitTestResult; use crate::dom::node::{self, Node, ShadowIncluding}; use crate::dom::pointerevent::PointerId; +use crate::dom::scrolling_box::ScrollingBoxAxis; use crate::dom::types::{ ClipboardEvent, CompositionEvent, DataTransfer, Element, Event, EventTarget, GlobalScope, HTMLAnchorElement, KeyboardEvent, MouseEvent, PointerEvent, Touch, TouchEvent, TouchList, @@ -78,6 +79,8 @@ pub(crate) struct DocumentEventHandler { last_click_info: DomRefCell)>>, /// The element that is currently hovered by the cursor. current_hover_target: MutNullableDom, + /// The element that was most recently clicked. + most_recently_clicked_element: MutNullableDom, /// The most recent mouse movement point, used for processing `mouseleave` events. #[no_trace] most_recent_mousemove_point: Cell>>, @@ -100,6 +103,7 @@ impl DocumentEventHandler { mouse_move_event_index: Default::default(), last_click_info: Default::default(), current_hover_target: Default::default(), + most_recently_clicked_element: Default::default(), most_recent_mousemove_point: Default::default(), current_cursor: Default::default(), active_touch_points: Default::default(), @@ -581,6 +585,8 @@ impl DocumentEventHandler { match event.action { // https://w3c.github.io/uievents/#handle-native-mouse-click MouseButtonAction::Click => { + self.most_recently_clicked_element.set(Some(&el)); + el.set_click_in_progress(true); dom_event.dispatch(node.upcast(), false, can_gc); el.set_click_in_progress(false); @@ -1472,4 +1478,70 @@ impl DocumentEventHandler { document.handle_element_scroll_event(&element); } } + + pub(crate) fn run_default_keyboard_event_handler(&self, event: &KeyboardEvent) { + if event.upcast::().type_() != atom!("keydown") { + return; + } + if !event.modifiers().is_empty() { + return; + } + + let scroll_axis = match event.key() { + Key::Named( + NamedKey::Home | + NamedKey::End | + NamedKey::PageDown | + NamedKey::PageUp | + NamedKey::ArrowUp | + NamedKey::ArrowDown, + ) => ScrollingBoxAxis::Y, + Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) => ScrollingBoxAxis::X, + _ => return, + }; + + let document = self.window.Document(); + let mut scrolling_box = document + .get_focused_element() + .or(self.most_recently_clicked_element.get()) + .and_then(|element| element.scrolling_box(ScrollContainerQueryFlags::Inclusive)) + .unwrap_or_else(|| document.viewport_scrolling_box()); + + while !scrolling_box.can_keyboard_scroll_in_axis(scroll_axis) { + // Always fall back to trying to scroll the entire document. + if scrolling_box.is_viewport() { + break; + } + let parent = scrolling_box + .parent() + .unwrap_or_else(|| document.viewport_scrolling_box()); + scrolling_box = parent; + } + + const LINE_HEIGHT: f32 = 76.0; + const LINE_WIDTH: f32 = 76.0; + + let current_scroll_offset = scrolling_box.scroll_position(); + let delta = match event.key() { + Key::Named(NamedKey::Home) => Vector2D::new(0.0, -current_scroll_offset.y), + Key::Named(NamedKey::End) => Vector2D::new( + 0.0, + -current_scroll_offset.y + scrolling_box.content_size().height - + scrolling_box.size().height, + ), + Key::Named(NamedKey::PageDown) => { + Vector2D::new(0.0, scrolling_box.size().height - 2.0 * LINE_HEIGHT) + }, + Key::Named(NamedKey::PageUp) => { + Vector2D::new(0.0, 2.0 * LINE_HEIGHT - scrolling_box.size().height) + }, + Key::Named(NamedKey::ArrowUp) => Vector2D::new(0.0, -LINE_HEIGHT), + Key::Named(NamedKey::ArrowDown) => Vector2D::new(0.0, LINE_HEIGHT), + Key::Named(NamedKey::ArrowLeft) => Vector2D::new(-LINE_WIDTH, 0.0), + Key::Named(NamedKey::ArrowRight) => Vector2D::new(LINE_WIDTH, 0.0), + _ => return, + }; + + scrolling_box.scroll_to(delta + current_scroll_offset, ScrollBehavior::Auto); + } } diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index 57dfa35e501..07a46384e8e 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -25,7 +25,7 @@ use html5ever::{LocalName, Namespace, Prefix, QualName, local_name, namespace_pr use js::jsapi::Heap; use js::jsval::JSVal; use js::rust::HandleObject; -use layout_api::{LayoutDamage, ScrollContainerQueryType, ScrollContainerResponse}; +use layout_api::{LayoutDamage, ScrollContainerQueryFlags, ScrollContainerResponse}; use net_traits::ReferrerPolicy; use net_traits::request::CorsSettings; use selectors::Element as SelectorsElement; @@ -61,7 +61,7 @@ use style::values::{AtomIdent, AtomString, CSSFloat, computed, specified}; use style::{ArcSlice, CaseSensitivityExt, dom_apis, thread_state}; use stylo_atoms::Atom; use stylo_dom::ElementState; -use webrender_api::units::{LayoutSize, LayoutVector2D}; +use webrender_api::units::LayoutVector2D; use xml5ever::serialize::TraversalScope::{ ChildrenOnly as XmlChildrenOnly, IncludeNode as XmlIncludeNode, }; @@ -166,6 +166,7 @@ use crate::dom::node::{ use crate::dom::nodelist::NodeList; use crate::dom::promise::Promise; use crate::dom::raredata::ElementRareData; +use crate::dom::scrolling_box::{ScrollingBox, ScrollingBoxSource}; use crate::dom::servoparser::ServoParser; use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot}; use crate::dom::text::Text; @@ -274,60 +275,6 @@ impl FromStr for AdjacentPosition { } } -/// Represents a scrolling box that can be either an element or the viewport -/// -enum ScrollingBox { - Element(DomRoot), - Viewport(DomRoot), -} - -impl ScrollingBox { - fn scroll_position(&self) -> LayoutVector2D { - match self { - ScrollingBox::Element(element) => element - .owner_window() - .scroll_offset_query(element.upcast::()), - ScrollingBox::Viewport(document) => document.window().scroll_offset(), - } - } - - fn size(&self) -> LayoutSize { - match self { - ScrollingBox::Element(element) => element.client_rect().size.to_f32().cast_unit(), - ScrollingBox::Viewport(document) => { - document.window().viewport_details().size.cast_unit() - }, - } - } - - fn parent(&self) -> Option { - match self { - ScrollingBox::Element(element) => element.scrolling_box(), - ScrollingBox::Viewport(_) => None, - } - } - - fn node(&self) -> &Node { - match self { - ScrollingBox::Element(element) => element.upcast(), - ScrollingBox::Viewport(document) => document.upcast(), - } - } - - pub(crate) fn scroll_to(&self, position: LayoutVector2D, behavior: ScrollBehavior) { - match self { - ScrollingBox::Element(element) => { - element - .owner_window() - .scroll_an_element(element, position.x, position.y, behavior); - }, - ScrollingBox::Viewport(document) => { - document.window().scroll(position.x, position.y, behavior); - }, - } - } -} - // // Element methods // @@ -889,16 +836,19 @@ impl Element { } #[allow(unsafe_code)] - fn scrolling_box(&self) -> Option { + pub(crate) fn scrolling_box(&self, flags: ScrollContainerQueryFlags) -> Option { self.owner_window() - .scroll_container_query(self.upcast(), ScrollContainerQueryType::ForScrollIntoView) + .scroll_container_query(self.upcast(), flags) .and_then(|response| match response { - ScrollContainerResponse::Viewport => { - Some(ScrollingBox::Viewport(self.owner_document())) - }, - ScrollContainerResponse::Element(parent_node_address) => { + ScrollContainerResponse::Viewport => Some(ScrollingBox::new( + ScrollingBoxSource::Viewport(self.owner_document()), + )), + ScrollContainerResponse::Element(parent_node_address, axes_overflow) => { let node = unsafe { from_untrusted_node_address(parent_node_address) }; - Some(ScrollingBox::Element(DomRoot::downcast(node)?)) + Some(ScrollingBox::new(ScrollingBoxSource::Element( + DomRoot::downcast(node)?, + axes_overflow, + ))) }, }) } @@ -913,7 +863,7 @@ impl Element { ) { // Step 1: For each ancestor element or viewport that establishes a scrolling box `scrolling // box`, in order of innermost to outermost scrolling box, run these substeps: - let mut parent_scrolling_box = self.scrolling_box(); + let mut parent_scrolling_box = self.scrolling_box(ScrollContainerQueryFlags::empty()); while let Some(scrolling_box) = parent_scrolling_box { parent_scrolling_box = scrolling_box.parent(); @@ -987,21 +937,22 @@ impl Element { // to follow it using our own geometry types. // // TODO: This makes the code below wrong for the purposes of writing modes. - let (adjusted_element_top_left, adjusted_element_bottom_right) = match scrolling_box { - ScrollingBox::Viewport(_) => (target_top_left, target_bottom_right), - ScrollingBox::Element(scrolling_element) => { - let scrolling_padding_rect_top_left = scrolling_element - .upcast::() - .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 (adjusted_element_top_left, adjusted_element_bottom_right) = + match scrolling_box.target() { + ScrollingBoxSource::Viewport(_) => (target_top_left, target_bottom_right), + ScrollingBoxSource::Element(scrolling_element, _) => { + let scrolling_padding_rect_top_left = scrolling_element + .upcast::() + .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 scrolling_box_size = scrolling_box.size(); let current_scroll_position = scrolling_box.scroll_position(); @@ -5062,7 +5013,7 @@ impl Element { .inspect(|states| states.for_each_state(callback)); } - fn client_rect(&self) -> Rect { + pub(crate) fn client_rect(&self) -> Rect { let doc = self.node.owner_doc(); if let Some(rect) = self diff --git a/components/script/dom/html/htmlelement.rs b/components/script/dom/html/htmlelement.rs index 90196897106..43eb5206633 100644 --- a/components/script/dom/html/htmlelement.rs +++ b/components/script/dom/html/htmlelement.rs @@ -9,7 +9,7 @@ use std::rc::Rc; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, QualName, local_name, ns}; use js::rust::HandleObject; -use layout_api::{QueryMsg, ScrollContainerQueryType, ScrollContainerResponse}; +use layout_api::{QueryMsg, ScrollContainerQueryFlags, ScrollContainerResponse}; use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods; use style::attr::AttrValue; use stylo_dom::ElementState; @@ -450,10 +450,10 @@ impl HTMLElementMethods for HTMLElement { #[allow(unsafe_code)] fn GetScrollParent(&self) -> Option> { self.owner_window() - .scroll_container_query(self.upcast(), ScrollContainerQueryType::ForScrollParent) + .scroll_container_query(self.upcast(), ScrollContainerQueryFlags::ForScrollParent) .and_then(|response| match response { ScrollContainerResponse::Viewport => self.owner_document().GetScrollingElement(), - ScrollContainerResponse::Element(parent_node_address) => { + ScrollContainerResponse::Element(parent_node_address, _) => { let node = unsafe { from_untrusted_node_address(parent_node_address) }; DomRoot::downcast(node) }, diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index 796af3f30e1..f80d9be4eaf 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -428,6 +428,7 @@ pub(crate) mod resizeobserverentry; pub(crate) mod resizeobserversize; pub(crate) mod response; pub(crate) mod screen; +mod scrolling_box; pub(crate) mod securitypolicyviolationevent; pub(crate) mod selection; #[allow(dead_code)] diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 85e7ceee0b6..d0df630a85b 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -121,6 +121,7 @@ use crate::dom::shadowroot::{IsUserAgentWidget, LayoutShadowRootHelpers, ShadowR use crate::dom::stylesheetlist::StyleSheetListOwner; use crate::dom::svgsvgelement::{LayoutSVGSVGElementHelpers, SVGSVGElement}; use crate::dom::text::Text; +use crate::dom::types::KeyboardEvent; use crate::dom::virtualmethods::{VirtualMethods, vtable_for}; use crate::dom::window::Window; use crate::script_runtime::CanGc; @@ -4084,6 +4085,14 @@ impl VirtualMethods for Node { self.ranges().drain_to_parent(context, self); } } + + fn handle_event(&self, event: &Event, _: CanGc) { + if let Some(event) = event.downcast::() { + self.owner_document() + .event_handler() + .run_default_keyboard_event_handler(event); + } + } } /// A summary of the changes that happened to a node. diff --git a/components/script/dom/scrolling_box.rs b/components/script/dom/scrolling_box.rs new file mode 100644 index 00000000000..cef1489ba77 --- /dev/null +++ b/components/script/dom/scrolling_box.rs @@ -0,0 +1,147 @@ +/* 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 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 webrender_api::units::{LayoutSize, LayoutVector2D}; + +use crate::dom::node::{Node, NodeTraits}; +use crate::dom::types::{Document, Element}; + +pub(crate) struct ScrollingBox { + target: ScrollingBoxSource, + cached_content_size: Cell>, + cached_size: Cell>, +} + +/// Represents a scrolling box that can be either an element or the viewport +/// +pub(crate) enum ScrollingBoxSource { + Element(DomRoot, AxesOverflow), + Viewport(DomRoot), +} + +#[derive(Copy, Clone)] +pub(crate) enum ScrollingBoxAxis { + X, + Y, +} + +impl ScrollingBox { + pub(crate) fn new(target: ScrollingBoxSource) -> Self { + Self { + target, + 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::()), + 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 { + 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 axes_overflow = match &self.target { + ScrollingBoxSource::Element(_, axes_overflow) => axes_overflow, + ScrollingBoxSource::Viewport(_) => return true, + }; + let overflow = match axis { + ScrollingBoxAxis::X => axes_overflow.x, + ScrollingBoxAxis::Y => axes_overflow.x, + }; + if !overflow.is_scrollable() || 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, + } + } +} diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 6c8f803ef52..5de2681365c 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -57,7 +57,7 @@ use layout_api::{ BoxAreaType, ElementsFromPointFlags, ElementsFromPointResult, FragmentType, Layout, LayoutImageDestination, PendingImage, PendingImageState, PendingRasterizationImage, QueryMsg, ReflowGoal, ReflowPhasesRun, ReflowRequest, ReflowRequestRestyle, RestyleReason, - ScrollContainerQueryType, ScrollContainerResponse, TrustedNodeAddress, + ScrollContainerQueryFlags, ScrollContainerResponse, TrustedNodeAddress, combine_id_with_fragment_type, }; use malloc_size_of::MallocSizeOf; @@ -2629,12 +2629,12 @@ impl Window { pub(crate) fn scroll_container_query( &self, node: &Node, - query_type: ScrollContainerQueryType, + flags: ScrollContainerQueryFlags, ) -> Option { self.layout_reflow(QueryMsg::ScrollParentQuery); self.layout .borrow() - .query_scroll_container(node.to_trusted_node_address(), query_type) + .query_scroll_container(node.to_trusted_node_address(), flags) } pub(crate) fn text_index_query( diff --git a/components/shared/layout/lib.rs b/components/shared/layout/lib.rs index dc54ae3587a..5aa76e5ed11 100644 --- a/components/shared/layout/lib.rs +++ b/components/shared/layout/lib.rs @@ -50,10 +50,11 @@ use style::data::ElementData; use style::dom::OpaqueNode; use style::invalidation::element::restyle_hints::RestyleHint; use style::media_queries::Device; -use style::properties::PropertyId; use style::properties::style_structs::Font; +use style::properties::{ComputedValues, PropertyId}; use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot}; use style::stylesheets::{Stylesheet, UrlExtraData}; +use style::values::computed::Overflow; use style_traits::CSSPixel; use webrender_api::units::{DeviceIntSize, LayoutPoint, LayoutVector2D}; use webrender_api::{ExternalScrollId, ImageKey}; @@ -308,7 +309,7 @@ pub trait Layout { fn query_scroll_container( &self, node: TrustedNodeAddress, - query_type: ScrollContainerQueryType, + flags: ScrollContainerQueryFlags, ) -> Option; fn query_resolved_style( &self, @@ -366,16 +367,44 @@ pub struct OffsetParentResponse { pub rect: Rect, } -#[derive(PartialEq)] -pub enum ScrollContainerQueryType { - ForScrollParent, - ForScrollIntoView, +bitflags! { + #[derive(PartialEq)] + pub struct ScrollContainerQueryFlags: u8 { + /// Whether or not this query is for the purposes of a `scrollParent` layout query. + const ForScrollParent = 1 << 0; + /// Whether or not to consider the original element's scroll box for the return value. + const Inclusive = 1 << 1; + } +} + +#[derive(Clone, Copy, Debug)] +pub struct AxesOverflow { + pub x: Overflow, + pub y: Overflow, +} + +impl Default for AxesOverflow { + fn default() -> Self { + Self { + x: Overflow::Visible, + y: Overflow::Visible, + } + } +} + +impl From<&ComputedValues> for AxesOverflow { + fn from(style: &ComputedValues) -> Self { + Self { + x: style.clone_overflow_x(), + y: style.clone_overflow_y(), + } + } } #[derive(Clone)] pub enum ScrollContainerResponse { Viewport, - Element(UntrustedNodeAddress), + Element(UntrustedNodeAddress, AxesOverflow), } #[derive(Debug, PartialEq)] diff --git a/ports/servoshell/desktop/app_state.rs b/ports/servoshell/desktop/app_state.rs index 84174159bfc..c8eefe94c33 100644 --- a/ports/servoshell/desktop/app_state.rs +++ b/ports/servoshell/desktop/app_state.rs @@ -11,14 +11,12 @@ use std::rc::Rc; use crossbeam_channel::Receiver; use embedder_traits::webdriver::WebDriverSenders; -use euclid::Vector2D; -use keyboard_types::{Key, Modifiers, NamedKey, ShortcutMatcher}; +use keyboard_types::ShortcutMatcher; use log::{error, info}; use servo::base::generic_channel::GenericSender; use servo::base::id::WebViewId; use servo::config::pref; use servo::ipc_channel::ipc::IpcSender; -use servo::webrender_api::ScrollLocation; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize}; use servo::{ AllowOrDenyRequest, AuthenticationRequest, FilterPattern, FormControl, GamepadHapticEffectType, @@ -32,7 +30,7 @@ use super::app::PumpResult; use super::dialog::Dialog; use super::gamepad::GamepadSupport; use super::keyutils::CMD_OR_CONTROL; -use super::window_trait::{LINE_HEIGHT, LINE_WIDTH, WindowPortsMethods}; +use super::window_trait::WindowPortsMethods; use crate::output_image::save_output_image_if_necessary; use crate::prefs::ServoShellPreferences; @@ -414,7 +412,6 @@ impl RunningAppState { /// Handle servoshell key bindings that may have been prevented by the page in the focused webview. fn handle_overridable_key_bindings(&self, webview: ::servo::WebView, event: KeyboardEvent) { - let origin = webview.rect().min.ceil().to_i32(); ShortcutMatcher::from_event(event.event) .shortcut(CMD_OR_CONTROL, '=', || { webview.set_zoom(1.1); @@ -427,42 +424,6 @@ impl RunningAppState { }) .shortcut(CMD_OR_CONTROL, '0', || { webview.reset_zoom(); - }) - .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageDown), || { - let scroll_location = ScrollLocation::Delta(Vector2D::new( - 0.0, - self.inner().window.page_height() - 2.0 * LINE_HEIGHT, - )); - webview.notify_scroll_event(scroll_location, origin); - }) - .shortcut(Modifiers::empty(), Key::Named(NamedKey::PageUp), || { - let scroll_location = ScrollLocation::Delta(Vector2D::new( - 0.0, - -self.inner().window.page_height() + 2.0 * LINE_HEIGHT, - )); - webview.notify_scroll_event(scroll_location, origin); - }) - .shortcut(Modifiers::empty(), Key::Named(NamedKey::Home), || { - webview.notify_scroll_event(ScrollLocation::Start, origin); - }) - .shortcut(Modifiers::empty(), Key::Named(NamedKey::End), || { - webview.notify_scroll_event(ScrollLocation::End, origin); - }) - .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowUp), || { - let location = ScrollLocation::Delta(Vector2D::new(0.0, -LINE_HEIGHT)); - webview.notify_scroll_event(location, origin); - }) - .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowDown), || { - let location = ScrollLocation::Delta(Vector2D::new(0.0, LINE_HEIGHT)); - webview.notify_scroll_event(location, origin); - }) - .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowLeft), || { - let location = ScrollLocation::Delta(Vector2D::new(-LINE_WIDTH, 0.0)); - webview.notify_scroll_event(location, origin); - }) - .shortcut(Modifiers::empty(), Key::Named(NamedKey::ArrowRight), || { - let location = ScrollLocation::Delta(Vector2D::new(LINE_WIDTH, 0.0)); - webview.notify_scroll_event(location, origin); }); } diff --git a/ports/servoshell/desktop/headed_window.rs b/ports/servoshell/desktop/headed_window.rs index 32ae5b5a9e7..b0f3105a8b9 100644 --- a/ports/servoshell/desktop/headed_window.rs +++ b/ports/servoshell/desktop/headed_window.rs @@ -532,12 +532,6 @@ impl WindowPortsMethods for Window { .unwrap_or_else(|| self.device_hidpi_scale_factor()) } - fn page_height(&self) -> f32 { - let dpr = self.hidpi_scale_factor(); - let size = self.winit_window.inner_size(); - size.height as f32 * dpr.get() - } - fn set_title(&self, title: &str) { self.winit_window.set_title(title); } diff --git a/ports/servoshell/desktop/headless_window.rs b/ports/servoshell/desktop/headless_window.rs index 92b661a88b6..9910ed941d4 100644 --- a/ports/servoshell/desktop/headless_window.rs +++ b/ports/servoshell/desktop/headless_window.rs @@ -118,12 +118,6 @@ impl WindowPortsMethods for Window { .unwrap_or_else(|| self.device_hidpi_scale_factor()) } - fn page_height(&self) -> f32 { - let height = self.inner_size.get().height; - let dpr = self.hidpi_scale_factor(); - height as f32 * dpr.get() - } - fn set_fullscreen(&self, state: bool) { self.fullscreen.set(state); } diff --git a/ports/servoshell/desktop/window_trait.rs b/ports/servoshell/desktop/window_trait.rs index d3c7997fe69..7915456b9dc 100644 --- a/ports/servoshell/desktop/window_trait.rs +++ b/ports/servoshell/desktop/window_trait.rs @@ -17,6 +17,7 @@ use super::app_state::RunningAppState; // This should vary by zoom level and maybe actual text size (focused or under cursor) pub(crate) const LINE_HEIGHT: f32 = 76.0; pub(crate) const LINE_WIDTH: f32 = 76.0; + // MouseScrollDelta::PixelDelta is default for MacOS, which is high precision and very slow // in winit. Therefore we use a factor of 4.0 to make it more usable. // See https://github.com/servo/servo/pull/34063#discussion_r2197729507 @@ -31,7 +32,6 @@ pub trait WindowPortsMethods { fn screen_geometry(&self) -> ScreenGeometry; fn device_hidpi_scale_factor(&self) -> Scale; fn hidpi_scale_factor(&self) -> Scale; - fn page_height(&self) -> f32; fn get_fullscreen(&self) -> bool; fn handle_winit_event(&self, state: Rc, event: winit::event::WindowEvent); fn set_title(&self, _title: &str) {} diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index c0472bade2f..8ffa3208e93 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -12748,6 +12748,15 @@ {} ] ], + "keyboard-scrolling.html": [ + "2d9a0c40272d8af49f26de8dc49283df68b2d7b0", + [ + null, + { + "testdriver": true + } + ] + ], "matchMedia.html": [ "45a7ea268b1ebdba69e947b79d675cc9221428d4", [ diff --git a/tests/wpt/mozilla/tests/css/keyboard-scrolling.html b/tests/wpt/mozilla/tests/css/keyboard-scrolling.html new file mode 100644 index 00000000000..2d9a0c40272 --- /dev/null +++ b/tests/wpt/mozilla/tests/css/keyboard-scrolling.html @@ -0,0 +1,195 @@ + + +CSS test: Calc expressions with numbers should still serialize as calc() + + + + + + + + +
+
Lorem ipsum dolor sit amet,
+
+ + +
+
Lorem ipsum dolor sit amet,
+
+ + +
+
Lorem ipsum dolor sit amet,
+
+ + + + + +
+
Lorem ipsum dolor sit amet,
+
+ +
+ Lorem ipsum dolor sit amet, +
+ +