diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index a8866b85ab8b0..b954810e613d4 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -878,6 +878,7 @@ set(SOURCES Painting/ResolvedCSSFilter.cpp Painting/ResizeHandle.cpp Painting/Scrollbar.cpp + Painting/ScrollSnap.cpp Painting/ScrollState.cpp Painting/SVGMasking.cpp Painting/ViewportPaintable.cpp diff --git a/Libraries/LibWeb/CSS/ComputedValues.cpp b/Libraries/LibWeb/CSS/ComputedValues.cpp index 8153d0dcf40f2..d8752fcd70b49 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.cpp +++ b/Libraries/LibWeb/CSS/ComputedValues.cpp @@ -260,6 +260,9 @@ static void register_style_group_field_descriptors() add(misc_reset, PropertyID::ViewTransitionName, 0, GROUP_FIELD_REQUIRE_KEYWORD, to_underlying(Keyword::None), nullptr); add(misc_reset, PropertyID::TouchAction, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); add(misc_reset, PropertyID::ScrollBehavior, offsetof(MiscReset, scroll_behavior), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::ScrollSnapAlign, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); + add(misc_reset, PropertyID::ScrollSnapStop, offsetof(MiscReset, scroll_snap_stop), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); + add(misc_reset, PropertyID::ScrollSnapType, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); add(misc_reset, PropertyID::ScrollbarGutter, 0, GROUP_FIELD_REQUIRE_INITIAL_VALUE, 0, nullptr); add(misc_reset, PropertyID::ScrollbarWidth, offsetof(MiscReset, scrollbar_width), GROUP_FIELD_ENUM_KEYWORD, 0, &keyword_code_table()); add(misc_reset, PropertyID::ShapeImageThreshold, offsetof(MiscReset, shape_image_threshold), GROUP_FIELD_RESOLVED_F64, 0, nullptr); @@ -1083,6 +1086,22 @@ TouchActionData ComputedValues::MiscResetValues::touch_action_value() const }; } +ScrollSnapAlignData ComputedValues::MiscResetValues::scroll_snap_align_value() const +{ + return { + .block_alignment = static_cast(scroll_snap_align_block), + .inline_alignment = static_cast(scroll_snap_align_inline), + }; +} + +ScrollSnapType ComputedValues::MiscResetValues::scroll_snap_type_value() const +{ + return { + .axis = static_cast(scroll_snap_axis), + .strictness = static_cast(scroll_snap_strictness), + }; +} + ShapeOutsideData ComputedValues::MiscResetValues::shape_outside_value() const { ShapeOutsideData result; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 2c58f6cc703b4..c97948cad2204 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -335,6 +335,20 @@ struct ScrollbarColorData { bool operator==(ScrollbarColorData const&) const = default; }; +struct ScrollSnapType { + ScrollSnapAxis axis { ScrollSnapAxis::Both }; + ScrollSnapStrictness strictness { ScrollSnapStrictness::None }; + + bool operator==(ScrollSnapType const&) const = default; +}; + +struct ScrollSnapAlignData { + ScrollSnapAlign block_alignment { ScrollSnapAlign::None }; + ScrollSnapAlign inline_alignment { ScrollSnapAlign::None }; + + bool operator==(ScrollSnapAlignData const&) const = default; +}; + struct TextIndentData { LengthPercentage length_percentage; bool each_line { false }; @@ -563,6 +577,9 @@ class InitialValues { static int math_depth() { return 0; } static ScrollBehavior scroll_behavior() { return ScrollBehavior::Auto; } + static ScrollSnapAlignData scroll_snap_align() { return {}; } + static ScrollSnapStop scroll_snap_stop() { return ScrollSnapStop::Normal; } + static ScrollSnapType scroll_snap_type() { return {}; } static ScrollbarColorData scrollbar_color() { return ScrollbarColorData { @@ -1628,6 +1645,9 @@ class WEB_API ComputedValues final : public RefCounted { int math_depth() const { return m_inherited.font->math_depth; } ScrollBehavior scroll_behavior() const { return static_cast(m_noninherited.misc->scroll_behavior); } + ScrollSnapAlignData scroll_snap_align() const { return m_noninherited.misc->scroll_snap_align_value(); } + ScrollSnapStop scroll_snap_stop() const { return static_cast(m_noninherited.misc->scroll_snap_stop); } + ScrollSnapType scroll_snap_type() const { return m_noninherited.misc->scroll_snap_type_value(); } ScrollbarColorData scrollbar_color() const { return m_inherited.ui->scrollbar_color_value(); } ScrollbarGutter scrollbar_gutter() const { return static_cast(m_noninherited.misc->scrollbar_gutter); } ScrollbarWidth scrollbar_width() const { return static_cast(m_noninherited.misc->scrollbar_width); } @@ -2335,6 +2355,8 @@ class WEB_API ComputedValues final : public RefCounted { Position object_position_value() const; Optional view_transition_name_value() const; TouchActionData touch_action_value() const; + ScrollSnapAlignData scroll_snap_align_value() const; + ScrollSnapType scroll_snap_type_value() const; ShapeOutsideData shape_outside_value() const; WillChange will_change_value() const; diff --git a/Libraries/LibWeb/CSS/Enums.json b/Libraries/LibWeb/CSS/Enums.json index bf667dcca7262..4d528109cecf1 100644 --- a/Libraries/LibWeb/CSS/Enums.json +++ b/Libraries/LibWeb/CSS/Enums.json @@ -962,6 +962,28 @@ "auto", "smooth" ], + "scroll-snap-align": [ + "none", + "start", + "end", + "center" + ], + "scroll-snap-axis": [ + "x", + "y", + "block", + "inline", + "both" + ], + "scroll-snap-stop": [ + "normal", + "always" + ], + "scroll-snap-strictness": [ + "none", + "proximity", + "mandatory" + ], "scrollbar-gutter": [ "auto", "stable", diff --git a/Libraries/LibWeb/CSS/Keywords.json b/Libraries/LibWeb/CSS/Keywords.json index b2f47f2cb61d0..84cf377308e80 100644 --- a/Libraries/LibWeb/CSS/Keywords.json +++ b/Libraries/LibWeb/CSS/Keywords.json @@ -408,6 +408,7 @@ "maithili", "malayalam-alpha", "malayalam", + "mandatory", "manipulation", "manipuri", "marathi", @@ -547,6 +548,7 @@ "prophoto-rgb", "proportional-nums", "proportional-width", + "proximity", "punjabi", "push-button", "r", diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index 7aae1955759bd..ba972839a2480 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -572,6 +572,7 @@ class Parser { RefPtr parse_position_visibility_value(TokenStream&); RefPtr parse_quotes_value(TokenStream&); RefPtr parse_single_repeat_style_value(PropertyID, TokenStream&); + RefPtr parse_scroll_snap_type_value(TokenStream&); RefPtr parse_scroll_timeline_value(TokenStream&); RefPtr parse_scrollbar_color_value(TokenStream&); RefPtr parse_scrollbar_gutter_value(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp b/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp index dedadc89383b2..e32125bcc4834 100644 --- a/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/PropertyParsing.cpp @@ -616,6 +616,8 @@ Parser::ParseErrorOr> Parser::parse_css_value(Pr return parse_all_as(tokens, [this](auto& tokens) { return parse_quotes_value(tokens); }); case PropertyID::Rotate: return parse_all_as(tokens, [this](auto& tokens) { return parse_rotate_value(tokens); }); + case PropertyID::ScrollSnapType: + return parse_all_as(tokens, [this](auto& tokens) { return parse_scroll_snap_type_value(tokens); }); case PropertyID::ScrollbarColor: return parse_all_as(tokens, [this](auto& tokens) { return parse_scrollbar_color_value(tokens); }); case PropertyID::ScrollbarGutter: @@ -4842,6 +4844,31 @@ RefPtr Parser::parse_scrollbar_gutter_value(TokenStream Parser::parse_scroll_snap_type_value(TokenStream& tokens) +{ + // none | [ x | y | block | inline | both ] [ mandatory | proximity ]? + auto transaction = tokens.begin_transaction(); + + if (auto none = parse_specific_keyword_value(tokens, { { Keyword::None } })) { + transaction.commit(); + return none; + } + + auto axis = parse_specific_keyword_value(tokens, { { Keyword::X, Keyword::Y, Keyword::Block, Keyword::Inline, Keyword::Both } }); + if (!axis) + return nullptr; + + auto strictness = parse_specific_keyword_value(tokens, { { Keyword::Mandatory, Keyword::Proximity } }); + transaction.commit(); + + // Proximity is the default strictness, so the single-keyword form is the shortest serialization of such values. + if (!strictness || strictness->to_keyword() == Keyword::Proximity) + return axis; + + return StyleValueList::create(StyleValueVector { axis.release_nonnull(), strictness.release_nonnull() }, StyleValueList::Separator::Space); +} + RefPtr Parser::parse_grid_track_placement_shorthand_value(PropertyID property_id, TokenStream& tokens) { auto start_property = (property_id == PropertyID::GridColumn) ? PropertyID::GridColumnStart : PropertyID::GridRowStart; diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index 1f8fbf53cf50a..42bda113a3e85 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -4303,6 +4303,37 @@ ], "percentages-resolve-to": "length" }, + "scroll-snap-align": { + "style-group": "MiscResetValues", + "affects-layout": false, + "animation-type": "discrete", + "inherited": false, + "initial": "none", + "max-values": 2, + "requires-computation": "never", + "valid-types": [ + "scroll-snap-align" + ] + }, + "scroll-snap-stop": { + "style-group": "MiscResetValues", + "affects-layout": false, + "animation-type": "discrete", + "inherited": false, + "initial": "normal", + "requires-computation": "never", + "valid-types": [ + "scroll-snap-stop" + ] + }, + "scroll-snap-type": { + "style-group": "MiscResetValues", + "affects-layout": false, + "animation-type": "discrete", + "inherited": false, + "initial": "none", + "requires-computation": "never" + }, "scroll-timeline": { "affects-layout": false, "initial": "none block", diff --git a/Libraries/LibWeb/CSS/StyleInvalidation.cpp b/Libraries/LibWeb/CSS/StyleInvalidation.cpp index 84abaf14942da..380940346805a 100644 --- a/Libraries/LibWeb/CSS/StyleInvalidation.cpp +++ b/Libraries/LibWeb/CSS/StyleInvalidation.cpp @@ -423,6 +423,18 @@ RequiredInvalidationAfterStyleChange compute_property_invalidation(CSS::Property if (CSS::property_affects_scrollable_overflow(property_id)) invalidation.set_needs_scrollable_overflow_recalculation(); + // https://drafts.csswg.org/css-scroll-snap-1/#re-snap + // NB: A scroll snap property change moves the snap positions of a snap container without necessarily changing + // layout, so snap containers re-evaluate their scroll position for these properties as they do after a + // layout change. + if (CSS::property_affects_scrollable_overflow(property_id) + || AK::first_is_one_of(property_id, + CSS::PropertyID::ScrollSnapType, CSS::PropertyID::ScrollSnapAlign, CSS::PropertyID::ScrollSnapStop, + CSS::PropertyID::ScrollMarginTop, CSS::PropertyID::ScrollMarginRight, CSS::PropertyID::ScrollMarginBottom, CSS::PropertyID::ScrollMarginLeft, + CSS::PropertyID::ScrollPaddingTop, CSS::PropertyID::ScrollPaddingRight, CSS::PropertyID::ScrollPaddingBottom, CSS::PropertyID::ScrollPaddingLeft)) { + invalidation.needs_scroll_container_resnap = true; + } + if (CSS::property_affects_stacking_context(property_id)) { // z-index changes always require rebuilding the stacking context tree because // the value determines painting order within the tree, not just whether a diff --git a/Libraries/LibWeb/CSS/StyleInvalidation.h b/Libraries/LibWeb/CSS/StyleInvalidation.h index e2e63677823f9..a676ab30cee5e 100644 --- a/Libraries/LibWeb/CSS/StyleInvalidation.h +++ b/Libraries/LibWeb/CSS/StyleInvalidation.h @@ -65,6 +65,8 @@ struct RequiredInvalidationAfterStyleChange { [[nodiscard]] bool needs_scrollable_overflow_recalculation() const { return m_needs_scrollable_overflow_recalculation && !needs_relayout(); } [[nodiscard]] AccumulatedVisualContextInvalidation accumulated_visual_contexts() const { return m_accumulated_visual_contexts; } + // A scroll snap property changed, so snap containers must re-evaluate their scroll position and re-snap. + bool needs_scroll_container_resnap : 1 { false }; // The element's change affects rule matching for descendants, without necessarily changing inherited style. bool recompute_descendant_styles : 1 { false }; // Names the inherited ComputedValues groups whose identities changed. Descendants can use the @@ -98,6 +100,7 @@ struct RequiredInvalidationAfterStyleChange { m_accumulated_visual_contexts = max(m_accumulated_visual_contexts, other.m_accumulated_visual_contexts); m_rebuild_stacking_context_tree |= other.m_rebuild_stacking_context_tree; m_needs_scrollable_overflow_recalculation |= other.m_needs_scrollable_overflow_recalculation; + needs_scroll_container_resnap |= other.needs_scroll_container_resnap; recompute_descendant_styles |= other.recompute_descendant_styles; m_inherited_style_groups_changed |= other.m_inherited_style_groups_changed; changes_containing_block_establishment |= other.changes_containing_block_establishment; @@ -110,6 +113,7 @@ struct RequiredInvalidationAfterStyleChange { return m_level == InvalidationLevel::None && m_accumulated_visual_contexts == AccumulatedVisualContextInvalidation::None && !m_needs_scrollable_overflow_recalculation + && !needs_scroll_container_resnap && !recompute_descendant_styles && !inherited_style_changed() && !changes_containing_block_establishment diff --git a/Libraries/LibWeb/CSS/UpdateStyle.cpp b/Libraries/LibWeb/CSS/UpdateStyle.cpp index 1077f22fb7b46..e32ff5eb8aaeb 100644 --- a/Libraries/LibWeb/CSS/UpdateStyle.cpp +++ b/Libraries/LibWeb/CSS/UpdateStyle.cpp @@ -54,6 +54,9 @@ static void apply_element_style_invalidation_after_style_change(DOM::Element& el if (invalidation.needs_scrollable_overflow_recalculation()) element.document().schedule_scrollable_overflow_recalculation(element); + if (invalidation.needs_scroll_container_resnap) + element.document().schedule_scroll_container_resnap(); + if (invalidation.changes_containing_block_establishment) element.document().partial_relayout_invalidation().record_escape(DOM::PartialRelayoutEscapeReason::ContainingBlockEstablishmentChangedByStyleChange); diff --git a/Libraries/LibWeb/Compositor/AsyncScrollTree.cpp b/Libraries/LibWeb/Compositor/AsyncScrollTree.cpp index 559489af18be6..ddcc2ae249cb5 100644 --- a/Libraries/LibWeb/Compositor/AsyncScrollTree.cpp +++ b/Libraries/LibWeb/Compositor/AsyncScrollTree.cpp @@ -342,8 +342,24 @@ Optional AsyncScrollTree::scroll_node_id_for_stable_id(AsyncS return {}; } -WheelHitTestResult AsyncScrollTree::hit_test_scroll_node_for_wheel(Gfx::FloatPoint position, Gfx::FloatPoint delta) const +WheelHitTestResult AsyncScrollTree::hit_test_scroll_node_for_wheel(Gfx::FloatPoint position, Gfx::FloatPoint delta, SnapContainerHandling snap_container_handling) const { + auto scrolled_on_the_main_thread_instead = [&](WheelHitTestResult const& result) { + if (snap_container_handling == SnapContainerHandling::ScrollOnCompositor || !result.node_id.has_value()) + return false; + auto const* node = scroll_node_for_id(*result.node_id); + if (!node) + return false; + return (node->snaps_scroll_position_horizontally && delta.x() != 0) + || (node->snaps_scroll_position_vertically && delta.y() != 0); + }; + auto hit_test_result_for_wheel_scroll_of_node = [&](AsyncScrollNodeID node_id) { + auto result = hit_test_result_for_scroll_node(node_id, delta); + if (scrolled_on_the_main_thread_instead(result)) + return WheelHitTestResult { {}, true }; + return result; + }; + if (!m_visual_context_tree) return {}; @@ -377,7 +393,7 @@ WheelHitTestResult AsyncScrollTree::hit_test_scroll_node_for_wheel(Gfx::FloatPoi continue; if (!target.target_node_id.has_value()) return {}; - return hit_test_result_for_scroll_node(*target.target_node_id, delta); + return hit_test_result_for_wheel_scroll_of_node(*target.target_node_id); } auto viewport_node_id = viewport_scroll_node_id(); @@ -386,7 +402,7 @@ WheelHitTestResult AsyncScrollTree::hit_test_scroll_node_for_wheel(Gfx::FloatPoi auto const* viewport_node = scroll_node_for_id(*viewport_node_id); if (!viewport_node || !viewport_node->scrollport_rect.to_type().contains(position)) return {}; - return hit_test_result_for_scroll_node(*viewport_node_id, delta); + return hit_test_result_for_wheel_scroll_of_node(*viewport_node_id); } bool AsyncScrollTree::scroll_node_is_viewport(AsyncScrollNodeID node_id) const diff --git a/Libraries/LibWeb/Compositor/AsyncScrollTree.h b/Libraries/LibWeb/Compositor/AsyncScrollTree.h index 681c598e60384..b6cb15ced3306 100644 --- a/Libraries/LibWeb/Compositor/AsyncScrollTree.h +++ b/Libraries/LibWeb/Compositor/AsyncScrollTree.h @@ -59,7 +59,7 @@ class WEB_API AsyncScrollTree { Optional scroll_offset_for_node(AsyncScrollNodeID, Painting::ScrollStateSnapshot const&) const; Optional viewport_scroll_node_id() const; Optional scroll_node_id_for_stable_id(AsyncScrollNodeStableID) const; - WheelHitTestResult hit_test_scroll_node_for_wheel(Gfx::FloatPoint position, Gfx::FloatPoint delta) const; + WheelHitTestResult hit_test_scroll_node_for_wheel(Gfx::FloatPoint position, Gfx::FloatPoint delta, SnapContainerHandling) const; bool scroll_node_is_viewport(AsyncScrollNodeID) const; Vector apply_scroll_delta(AsyncScrollNodeID, Gfx::FloatPoint delta, Painting::ScrollStateSnapshot&); Optional set_scroll_offset(AsyncScrollNodeID, Gfx::FloatPoint, Painting::ScrollStateSnapshot&); diff --git a/Libraries/LibWeb/Compositor/AsyncScrollingState.cpp b/Libraries/LibWeb/Compositor/AsyncScrollingState.cpp index e7a30b8ed7913..3e04fa13315ac 100644 --- a/Libraries/LibWeb/Compositor/AsyncScrollingState.cpp +++ b/Libraries/LibWeb/Compositor/AsyncScrollingState.cpp @@ -7,17 +7,25 @@ #include #include #include +#include #include #include namespace Web::Compositor { +SnapContainerHandling snap_container_handling_for(WheelDeltaPrecision wheel_delta_precision, ScrollGesturePhase scroll_gesture_phase) +{ + if (wheel_delta_precision == WheelDeltaPrecision::Discrete || scroll_gesture_phase == ScrollGesturePhase::Momentum) + return SnapContainerHandling::DeferToMainThread; + return SnapContainerHandling::ScrollOnCompositor; +} + static AsyncScrollNodeID scroll_node_id_for(UniqueNodeID document_id, Painting::VisualContextIndex scroll_node_index) { return { .document_id = document_id, .scroll_node_index = scroll_node_index }; } -static AsyncScrollNodeKind async_scroll_node_kind_for(Painting::CompositorScrollNodeKind kind) +AsyncScrollNodeKind async_scroll_node_kind_for(Painting::CompositorScrollNodeKind kind) { switch (kind) { case Painting::CompositorScrollNodeKind::Viewport: @@ -106,6 +114,8 @@ AsyncScrollingState async_scrolling_state_from_display_list(Painting::DisplayLis .is_viewport = command.is_viewport, .can_be_wheel_scrolled_horizontally = command.can_be_wheel_scrolled_horizontally, .can_be_wheel_scrolled_vertically = command.can_be_wheel_scrolled_vertically, + .snaps_scroll_position_horizontally = command.snaps_scroll_position_horizontally, + .snaps_scroll_position_vertically = command.snaps_scroll_position_vertically, }); parent_scroll_node_indices.append(command.parent_scroll_node_index); break; @@ -216,7 +226,7 @@ bool blocks_wheel_event_at_position(AsyncScrollingState const& async_scrolling_s return false; } -static WheelHitTestResult hit_test_scroll_node_at_position(AsyncScrollingState const& async_scrolling_state, RefPtr const& display_list, Painting::AccumulatedVisualContextTree const* visual_context_tree, Painting::ScrollStateSnapshot const& scroll_state_snapshot, Gfx::FloatPoint position, Gfx::FloatPoint delta) +static WheelHitTestResult hit_test_scroll_node_at_position(AsyncScrollingState const& async_scrolling_state, RefPtr const& display_list, Painting::AccumulatedVisualContextTree const* visual_context_tree, Painting::ScrollStateSnapshot const& scroll_state_snapshot, Gfx::FloatPoint position, Gfx::FloatPoint delta, SnapContainerHandling snap_container_handling) { if (!display_list || !visual_context_tree) return {}; @@ -225,12 +235,12 @@ static WheelHitTestResult hit_test_scroll_node_at_position(AsyncScrollingState c auto async_scrolling_state_copy = async_scrolling_state; scroll_tree.set_state(move(async_scrolling_state_copy)); scroll_tree.rebuild_wheel_hit_test_targets(display_list, visual_context_tree, scroll_state_snapshot); - return scroll_tree.hit_test_scroll_node_for_wheel(position, delta); + return scroll_tree.hit_test_scroll_node_for_wheel(position, delta, snap_container_handling); } -WheelScrollAdmission admit_wheel_scroll(AsyncScrollingState const& async_scrolling_state, RefPtr const& display_list, Painting::AccumulatedVisualContextTree const* visual_context_tree, Painting::ScrollStateSnapshot const& scroll_state_snapshot, Gfx::FloatPoint position, Gfx::FloatPoint delta, bool blocking_wheel_event_regions_are_current) +WheelScrollAdmission admit_wheel_scroll(AsyncScrollingState const& async_scrolling_state, RefPtr const& display_list, Painting::AccumulatedVisualContextTree const* visual_context_tree, Painting::ScrollStateSnapshot const& scroll_state_snapshot, Gfx::FloatPoint position, Gfx::FloatPoint delta, SnapContainerHandling snap_container_handling, bool blocking_wheel_event_regions_are_current) { - auto hit_test_result = hit_test_scroll_node_at_position(async_scrolling_state, display_list, visual_context_tree, scroll_state_snapshot, position, delta); + auto hit_test_result = hit_test_scroll_node_at_position(async_scrolling_state, display_list, visual_context_tree, scroll_state_snapshot, position, delta, snap_container_handling); if (hit_test_result.blocked_by_main_thread_region) return WheelScrollAdmission::BlockedByMainThreadRegion; diff --git a/Libraries/LibWeb/Compositor/AsyncScrollingState.h b/Libraries/LibWeb/Compositor/AsyncScrollingState.h index b1314f157931b..7d63c77190ec7 100644 --- a/Libraries/LibWeb/Compositor/AsyncScrollingState.h +++ b/Libraries/LibWeb/Compositor/AsyncScrollingState.h @@ -7,8 +7,10 @@ #pragma once #include +#include #include #include +#include #include #include #include @@ -38,6 +40,8 @@ enum class AsyncScrollNodeKind : u8 { PseudoElement, }; +WEB_API AsyncScrollNodeKind async_scroll_node_kind_for(Painting::CompositorScrollNodeKind); + // Stable identity for reconciling compositor-side scroll offsets after the paint snapshot has been rebuilt. struct AsyncScrollNodeStableID { UniqueNodeID node_id; @@ -64,6 +68,8 @@ struct AsyncScrollNode { bool is_viewport { false }; bool can_be_wheel_scrolled_horizontally { false }; bool can_be_wheel_scrolled_vertically { false }; + bool snaps_scroll_position_horizontally { false }; + bool snaps_scroll_position_vertically { false }; }; // Sticky elements are represented as scroll nodes whose offset is derived from ancestor scroll offsets. Keep only @@ -148,6 +154,16 @@ enum class WheelRoutingAdmission { StaleWheelEventListeners, }; +// A discrete wheel step and the momentum of a flick both scroll straight to the snap position they select, and only +// the main thread holds the snap positions to select from, so the compositor declines the deltas of either whose +// scrolling box snaps along an axis they travel in. +enum class SnapContainerHandling : u8 { + ScrollOnCompositor, + DeferToMainThread, +}; + +WEB_API SnapContainerHandling snap_container_handling_for(WheelDeltaPrecision, ScrollGesturePhase); + enum class WheelScrollAdmission { Accepted, NoScrollableTarget, @@ -160,6 +176,15 @@ WEB_API AsyncScrollingState async_scrolling_state_from_display_list(Painting::Di WEB_API WheelRoutingAdmission wheel_routing_admission_for(AsyncScrollingState const&); WEB_API Utf16View wheel_routing_admission_to_utf16_view(WheelRoutingAdmission); WEB_API bool blocks_wheel_event_at_position(AsyncScrollingState const&, RefPtr const&, Painting::AccumulatedVisualContextTree const*, Painting::ScrollStateSnapshot const&, Gfx::FloatPoint position); -WEB_API WheelScrollAdmission admit_wheel_scroll(AsyncScrollingState const&, RefPtr const&, Painting::AccumulatedVisualContextTree const*, Painting::ScrollStateSnapshot const&, Gfx::FloatPoint position, Gfx::FloatPoint delta, bool blocking_wheel_event_regions_are_current); +WEB_API WheelScrollAdmission admit_wheel_scroll(AsyncScrollingState const&, RefPtr const&, Painting::AccumulatedVisualContextTree const*, Painting::ScrollStateSnapshot const&, Gfx::FloatPoint position, Gfx::FloatPoint delta, SnapContainerHandling, bool blocking_wheel_event_regions_are_current); } + +template<> +struct AK::Traits : DefaultTraits { + static unsigned hash(Web::Compositor::AsyncScrollNodeStableID const& stable_node_id) + { + return pair_int_hash(u64_hash(static_cast(stable_node_id.node_id.value())), + pair_int_hash(to_underlying(stable_node_id.kind), stable_node_id.pseudo_element_type)); + } +}; diff --git a/Libraries/LibWeb/Compositor/CompositorHost.cpp b/Libraries/LibWeb/Compositor/CompositorHost.cpp index 2283ab59949ca..827eeb17dc9d9 100644 --- a/Libraries/LibWeb/Compositor/CompositorHost.cpp +++ b/Libraries/LibWeb/Compositor/CompositorHost.cpp @@ -71,14 +71,14 @@ void CompositorContextHandle::invalidate_wheel_event_listener_state(u64 generati } AsyncScrollEnqueueResult CompositorContextHandle::async_scroll_by(UniqueNodeID expected_document_id, Gfx::FloatPoint position, - Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, AsyncScrollOperationTracking operation_tracking) + Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, SnapContainerHandling snap_container_handling, AsyncScrollOperationTracking operation_tracking) { - return m_host.async_scroll_by(m_context_id, expected_document_id, position, delta_in_device_pixels, viewport_rect, operation_tracking); + return m_host.async_scroll_by(m_context_id, expected_document_id, position, delta_in_device_pixels, viewport_rect, snap_container_handling, operation_tracking); } -AsyncScrollEnqueueResult CompositorContextHandle::smooth_scroll_to(AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) +AsyncScrollEnqueueResult CompositorContextHandle::smooth_scroll_to(AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset_in_device_pixels, Gfx::FloatPoint main_thread_offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, ScrollAnimationKind animation_kind) { - return m_host.smooth_scroll_to(m_context_id, stable_node_id, offset_in_device_pixels, viewport_rect, device_pixels_per_css_pixel); + return m_host.smooth_scroll_to(m_context_id, stable_node_id, offset_in_device_pixels, main_thread_offset_in_device_pixels, viewport_rect, device_pixels_per_css_pixel, animation_kind); } void CompositorContextHandle::cancel_smooth_scroll(AsyncScrollNodeStableID stable_node_id) diff --git a/Libraries/LibWeb/Compositor/CompositorHost.h b/Libraries/LibWeb/Compositor/CompositorHost.h index 812e7f3bc325a..7cdbde4dd2759 100644 --- a/Libraries/LibWeb/Compositor/CompositorHost.h +++ b/Libraries/LibWeb/Compositor/CompositorHost.h @@ -44,8 +44,8 @@ class WEB_API CompositorContextHandle { void update_scroll_state(Painting::ScrollStateSnapshot&&); void invalidate_wheel_event_listener_state(u64 generation); AsyncScrollEnqueueResult async_scroll_by(UniqueNodeID expected_document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels, - Gfx::IntRect viewport_rect, AsyncScrollOperationTracking = AsyncScrollOperationTracking::No); - AsyncScrollEnqueueResult smooth_scroll_to(AsyncScrollNodeStableID, Gfx::FloatPoint offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel); + Gfx::IntRect viewport_rect, SnapContainerHandling, AsyncScrollOperationTracking = AsyncScrollOperationTracking::No); + AsyncScrollEnqueueResult smooth_scroll_to(AsyncScrollNodeStableID, Gfx::FloatPoint offset_in_device_pixels, Gfx::FloatPoint main_thread_offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, ScrollAnimationKind); void cancel_smooth_scroll(AsyncScrollNodeStableID); PendingAsyncScrollUpdates take_pending_async_scroll_updates(); void viewport_size_updated(Gfx::IntSize, WindowResizingInProgress); @@ -89,9 +89,9 @@ class WEB_API CompositorHost { virtual void update_scroll_state(CompositorContextId, Painting::ScrollStateSnapshot&&) = 0; virtual void invalidate_wheel_event_listener_state(CompositorContextId, u64 generation) = 0; virtual AsyncScrollEnqueueResult async_scroll_by(CompositorContextId, UniqueNodeID expected_document_id, Gfx::FloatPoint position, - Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, AsyncScrollOperationTracking) + Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, SnapContainerHandling, AsyncScrollOperationTracking) = 0; - virtual AsyncScrollEnqueueResult smooth_scroll_to(CompositorContextId, AsyncScrollNodeStableID, Gfx::FloatPoint offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) = 0; + virtual AsyncScrollEnqueueResult smooth_scroll_to(CompositorContextId, AsyncScrollNodeStableID, Gfx::FloatPoint offset_in_device_pixels, Gfx::FloatPoint main_thread_offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, ScrollAnimationKind) = 0; virtual void cancel_smooth_scroll(CompositorContextId, AsyncScrollNodeStableID) = 0; virtual PendingAsyncScrollUpdates take_pending_async_scroll_updates(CompositorContextId) = 0; virtual void viewport_size_updated(CompositorContextId, Gfx::IntSize, WindowResizingInProgress) = 0; diff --git a/Libraries/LibWeb/Compositor/SmoothScrollAnimation.cpp b/Libraries/LibWeb/Compositor/SmoothScrollAnimation.cpp index 0c4b8824f39a9..b598114a31bed 100644 --- a/Libraries/LibWeb/Compositor/SmoothScrollAnimation.cpp +++ b/Libraries/LibWeb/Compositor/SmoothScrollAnimation.cpp @@ -14,15 +14,31 @@ namespace Web::Compositor { static constexpr double scroll_speed_in_pixels_per_second = 1000.0; static constexpr double maximum_scroll_duration_in_seconds = 0.2; -SmoothScrollAnimation::SmoothScrollAnimation(Gfx::FloatPoint start_offset, Gfx::FloatPoint destination_offset, double pixels_per_css_pixel) +// The share of a frame's distance that the frame after it covers while momentum decays, and the length of the frames +// that share is measured in. +static constexpr double momentum_distance_share_per_frame = 0.92; +static constexpr double momentum_frame_duration_in_seconds = 0.016; +static constexpr double maximum_momentum_duration_in_seconds = 5.0; + +static double momentum_frames_for_distance(double distance) +{ + auto frames = AK::ceil(-AK::log(1 - distance * (1 - 1 / momentum_distance_share_per_frame)) / AK::log(momentum_distance_share_per_frame)); + return min(frames, maximum_momentum_duration_in_seconds / momentum_frame_duration_in_seconds); +} + +SmoothScrollAnimation::SmoothScrollAnimation(Gfx::FloatPoint start_offset, Gfx::FloatPoint destination_offset, double pixels_per_css_pixel, ScrollAnimationKind kind) : m_start_offset(start_offset) , m_destination_offset(destination_offset) + , m_kind(kind) { VERIFY(pixels_per_css_pixel > 0); auto horizontal_distance = static_cast(destination_offset.x() - start_offset.x()) / pixels_per_css_pixel; auto vertical_distance = static_cast(destination_offset.y() - start_offset.y()) / pixels_per_css_pixel; auto distance = AK::sqrt(horizontal_distance * horizontal_distance + vertical_distance * vertical_distance); - auto duration_in_seconds = min(distance / scroll_speed_in_pixels_per_second, maximum_scroll_duration_in_seconds); + + auto duration_in_seconds = kind == ScrollAnimationKind::Momentum + ? momentum_frames_for_distance(distance) * momentum_frame_duration_in_seconds + : min(distance / scroll_speed_in_pixels_per_second, maximum_scroll_duration_in_seconds); m_duration = AK::Duration::from_seconds_f64(duration_in_seconds); } @@ -33,11 +49,18 @@ SmoothScrollAnimation::Sample SmoothScrollAnimation::sample(AK::Duration elapsed auto progress = clamp(elapsed.to_seconds_f64() / m_duration.to_seconds_f64(), 0.0, 1.0); - // https://drafts.csswg.org/cssom-view/#smooth-scroll - // A smooth scroll follows a user-agent-defined timing function. Match the - // ease-in-out curve used by WebKit for programmatic smooth scrolling. - static CSS::CubicBezierEasingFunction const easing_function { 0.42, 0, 0.58, 1, {} }; - auto eased_progress = easing_function.evaluate_at(progress, false); + double eased_progress = 0; + if (m_kind == ScrollAnimationKind::Momentum) { + auto frames = m_duration.to_seconds_f64() / momentum_frame_duration_in_seconds; + auto frames_elapsed = progress * frames; + eased_progress = (1 - AK::pow(momentum_distance_share_per_frame, frames_elapsed)) / (1 - AK::pow(momentum_distance_share_per_frame, frames)); + } else { + // https://drafts.csswg.org/cssom-view/#smooth-scroll + // A smooth scroll follows a user-agent-defined timing function. Match the + // ease-in-out curve used by WebKit for programmatic smooth scrolling. + static CSS::CubicBezierEasingFunction const easing_function { 0.42, 0, 0.58, 1, {} }; + eased_progress = easing_function.evaluate_at(progress, false); + } return { { diff --git a/Libraries/LibWeb/Compositor/SmoothScrollAnimation.h b/Libraries/LibWeb/Compositor/SmoothScrollAnimation.h index a51a2fe747410..6f8668badd5aa 100644 --- a/Libraries/LibWeb/Compositor/SmoothScrollAnimation.h +++ b/Libraries/LibWeb/Compositor/SmoothScrollAnimation.h @@ -8,6 +8,7 @@ #include #include +#include #include namespace Web::Compositor { @@ -19,7 +20,7 @@ class WEB_API SmoothScrollAnimation { bool complete { false }; }; - SmoothScrollAnimation(Gfx::FloatPoint start_offset, Gfx::FloatPoint destination_offset, double pixels_per_css_pixel); + SmoothScrollAnimation(Gfx::FloatPoint start_offset, Gfx::FloatPoint destination_offset, double pixels_per_css_pixel, ScrollAnimationKind = ScrollAnimationKind::SmoothScroll); AK::Duration duration() const { return m_duration; } Sample sample(AK::Duration elapsed) const; @@ -28,6 +29,7 @@ class WEB_API SmoothScrollAnimation { Gfx::FloatPoint m_start_offset; Gfx::FloatPoint m_destination_offset; AK::Duration m_duration; + ScrollAnimationKind m_kind { ScrollAnimationKind::SmoothScroll }; }; } diff --git a/Libraries/LibWeb/Compositor/Types.cpp b/Libraries/LibWeb/Compositor/Types.cpp index 27e510344539a..2763572f12d0c 100644 --- a/Libraries/LibWeb/Compositor/Types.cpp +++ b/Libraries/LibWeb/Compositor/Types.cpp @@ -53,6 +53,9 @@ ErrorOr encode(Encoder& encoder, Web::Compositor::PendingAsyncScrollUpdate { TRY(encoder.encode(updates.scroll_offsets)); TRY(encoder.encode(updates.completed_operation_ids)); + TRY(encoder.encode(updates.operation_ids_taken_over_by_user_input)); + TRY(encoder.encode(updates.user_scroll_gesture_in_progress)); + TRY(encoder.encode(updates.user_scroll_gesture_ended)); return {}; } @@ -62,6 +65,9 @@ ErrorOr decode(Decoder& decoder) return Web::Compositor::PendingAsyncScrollUpdates { .scroll_offsets = TRY(decoder.decode>()), .completed_operation_ids = TRY(decoder.decode>()), + .operation_ids_taken_over_by_user_input = TRY(decoder.decode>()), + .user_scroll_gesture_in_progress = TRY(decoder.decode()), + .user_scroll_gesture_ended = TRY(decoder.decode()), }; } diff --git a/Libraries/LibWeb/Compositor/Types.h b/Libraries/LibWeb/Compositor/Types.h index c647265001585..fedb5ab9ccf62 100644 --- a/Libraries/LibWeb/Compositor/Types.h +++ b/Libraries/LibWeb/Compositor/Types.h @@ -40,6 +40,9 @@ enum class PagePresentationRegistration { struct PendingAsyncScrollUpdates { Vector scroll_offsets; Vector completed_operation_ids; + Vector operation_ids_taken_over_by_user_input; + bool user_scroll_gesture_in_progress { false }; + bool user_scroll_gesture_ended { false }; }; struct AsyncScrollEnqueueResult { @@ -52,6 +55,11 @@ enum class AsyncScrollOperationTracking { Yes, }; +enum class ScrollAnimationKind : u8 { + SmoothScroll, + Momentum, +}; + } namespace IPC { diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 1dd05f7ce609e..db23596880513 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -1839,6 +1840,8 @@ void Document::after_layout_commit(LayoutTreeChanged layout_tree_changed, Layout collect_paintable_boxes_with_auto_content_visibility(); } + schedule_scroll_container_resnap(); + m_document->set_needs_repaint(); } @@ -2123,6 +2126,12 @@ void Document::update_layout(UpdateLayoutReason reason) m_is_running_update_layout = true; ScopeGuard guard = [&] { m_is_running_update_layout = false; + + if (m_needs_scroll_container_resnap) { + if (auto navigable = this->navigable(); navigable && navigable->active_document().ptr() == this) + navigable->re_snap_scroll_containers_after_layout_change(); + } + page().client().flush_pending_dom_mutations(); }; @@ -8980,6 +8989,58 @@ void Document::schedule_scrollable_overflow_recalculation(Element& element) }); } +Painting::SnappedAreas const& Document::snapped_areas_of_scroll_container(Compositor::AsyncScrollNodeStableID const& stable_node_id) const +{ + static NeverDestroyed no_snapped_areas; + auto snapped_areas = m_scroll_container_snapped_areas.find(stable_node_id); + if (snapped_areas == m_scroll_container_snapped_areas.end()) + return *no_snapped_areas; + return snapped_areas->value; +} + +void Document::set_snapped_areas_of_scroll_container(Compositor::AsyncScrollNodeStableID const& stable_node_id, Painting::SnappedAreas snapped_areas) +{ + if (snapped_areas.is_empty()) { + m_scroll_container_snapped_areas.remove(stable_node_id); + return; + } + m_scroll_container_snapped_areas.set(stable_node_id, move(snapped_areas)); +} + +void Document::forget_snapped_areas_of_scroll_container(Painting::Paintable const& scroll_container) +{ + if (m_scroll_container_snapped_areas.is_empty()) + return; + if (auto stable_node_id = scroll_container.async_scroll_node_stable_id(); stable_node_id.has_value()) + m_scroll_container_snapped_areas.remove(*stable_node_id); +} + +void Document::register_scroll_snap_container(Painting::Paintable const& snap_container) +{ + if (any_of(m_scroll_snap_containers, [&](auto const& registered) { return registered.ptr() == &snap_container; })) + return; + m_scroll_snap_containers.append(snap_container.make_weak_ptr()); +} + +Vector> Document::collect_scroll_snap_containers() +{ + // A box replaced by a commit that rebuilt its node is no longer the paint tree's box for that node, so it stops + // being a snap container of this document. + m_scroll_snap_containers.remove_all_matching([](auto const& registered) { + return !registered || !registered->has_layout_node() || registered->layout_node().paintable_ptr() != registered.ptr(); + }); + + Vector> snap_containers; + snap_containers.ensure_capacity(m_scroll_snap_containers.size()); + for (auto const& registered : m_scroll_snap_containers) { + // The scroll snap properties of a registered box can stop making it a snap container without the paint tree + // being built again. + if (Painting::is_scroll_snap_container(*registered)) + snap_containers.unchecked_append(*registered); + } + return snap_containers; +} + void Document::set_needs_to_record_display_list() { m_hit_test_display_list = nullptr; diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index 4d244e1dd27aa..4b60b5e196a20 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -1133,6 +1135,19 @@ class WEB_API Document void schedule_scrollable_overflow_recalculation(Element&); void schedule_scrollable_overflow_recalculation(Layout::Node const&); + Painting::SnappedAreas const& snapped_areas_of_scroll_container(Compositor::AsyncScrollNodeStableID const&) const; + void set_snapped_areas_of_scroll_container(Compositor::AsyncScrollNodeStableID const&, Painting::SnappedAreas); + void forget_snapped_areas_of_scroll_container(Painting::Paintable const&); + + void schedule_scroll_container_resnap() { m_needs_scroll_container_resnap = true; } + void cancel_scheduled_scroll_container_resnap() { m_needs_scroll_container_resnap = false; } + [[nodiscard]] bool needs_scroll_container_resnap() const { return m_needs_scroll_container_resnap; } + void set_may_have_scroll_snap_areas() { m_may_have_scroll_snap_areas = true; } + [[nodiscard]] bool may_have_scroll_snap_areas() const { return m_may_have_scroll_snap_areas; } + + void register_scroll_snap_container(Painting::Paintable const&); + [[nodiscard]] Vector> collect_scroll_snap_containers(); + virtual Vector supported_property_names() const override; Vector> const& potentially_named_elements() const { return m_potentially_named_elements; } Vector> named_elements_with_name(Utf16FlyString const&) const; @@ -1817,6 +1832,11 @@ class WEB_API Document bool m_needs_full_scrollable_overflow_recalculation { false }; Vector> m_paintable_boxes_needing_scrollable_overflow_recalculation; + + HashMap m_scroll_container_snapped_areas; + Vector> m_scroll_snap_containers; + bool m_needs_scroll_container_resnap { false }; + bool m_may_have_scroll_snap_areas { false }; CSS::SheetSetStyleCacheRegistry m_sheet_set_style_cache_registry; RefPtr m_hit_test_display_list; // The previous recording's list, retained so cached per-paintable item ranges can be spliced into diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index ee241a5d042c2..de628e338841d 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -3302,13 +3302,13 @@ double Element::scroll_top() const if (document.document_element() == this && document.in_quirks_mode()) return 0.0; + // NOTE: Ensure that layout is up-to-date before looking at metrics. + const_cast(document).update_layout(UpdateLayoutReason::ElementScrollTop); + // 6. If the element is the root element return the value of scrollY on window. if (document.document_element() == this) return window->scroll_y(); - // NOTE: Ensure that layout is up-to-date before looking at metrics. - const_cast(document).update_layout(UpdateLayoutReason::ElementScrollTop); - // 7. If the element is the body element, document is in quirks mode, and the element is not potentially scrollable, return the value of scrollY on window. if (document.body() == this && document.in_quirks_mode() && !is_potentially_scrollable()) return window->scroll_y(); @@ -3347,13 +3347,13 @@ double Element::scroll_left() const if (document.document_element() == this && document.in_quirks_mode()) return 0.0; + // NOTE: Ensure that layout is up-to-date before looking at metrics. + const_cast(document).update_layout(UpdateLayoutReason::ElementScrollLeft); + // 6. If the element is the root element return the value of scrollX on window. if (document.document_element() == this) return window->scroll_x(); - // NOTE: Ensure that layout is up-to-date before looking at metrics. - const_cast(document).update_layout(UpdateLayoutReason::ElementScrollLeft); - // 7. If the element is the body element, document is in quirks mode, and the element is not potentially scrollable, return the value of scrollX on window. if (document.body() == this && document.in_quirks_mode() && !is_potentially_scrollable()) return window->scroll_x(); @@ -4999,7 +4999,7 @@ void Element::scroll(double x, double y, GC::Ptr promise) } // https://drafts.csswg.org/cssom-view/#dom-element-scroll -void Element::scroll(Bindings::ScrollToOptions options, GC::Ptr promise) +void Element::scroll(Bindings::ScrollToOptions options, GC::Ptr promise, Optional relative_displacement) { // 1. If invoked with one argument, follow these substeps: // 1. Let options be the argument. @@ -5045,9 +5045,13 @@ void Element::scroll(Bindings::ScrollToOptions options, GC::Ptr && scroll_offset({}).is_zero() && this != document.body() && this != document.document_element()) { - if (promise) - WebIDL::resolve_promise(*promise); - return; + document.update_style(); + auto const* misc_reset_values = style_group(); + if (!misc_reset_values || misc_reset_values->scroll_snap_type_value().strictness == CSS::ScrollSnapStrictness::None) { + if (promise) + WebIDL::resolve_promise(*promise); + return; + } } // NB: Ensure that layout is up-to-date before looking at metrics. @@ -5056,7 +5060,7 @@ void Element::scroll(Bindings::ScrollToOptions options, GC::Ptr // 8. If the element is the root element, return the Promise returned by scroll() on window after the method is // invoked with scrollX on window as first argument and y as second argument, and abort the remaining steps. if (document.document_element() == this) { - window->scroll(x, y, promise); + window->scroll(x, y, promise, relative_displacement); return; } @@ -5064,7 +5068,7 @@ void Element::scroll(Bindings::ScrollToOptions options, GC::Ptr // scrollable, return the Promise returned by scroll() on window after the method is invoked with options as the // only argument, and abort the remaining steps. if (document.body() == this && document.in_quirks_mode() && !is_potentially_scrollable()) { - window->scroll(x, y, promise); + window->scroll(x, y, promise, relative_displacement); return; } @@ -5081,7 +5085,7 @@ void Element::scroll(Bindings::ScrollToOptions options, GC::Ptr // options. Let scrollPromise be the Promise returned from this step. auto scroll_offset = CSSPixelPoint { CSSPixels::nearest_value_for(x), CSSPixels::nearest_value_for(y) }; if (auto navigable = document.navigable()) { - auto scroll_promise = navigable->perform_a_scroll_of_an_element(*this, scroll_offset, options.behavior); + auto scroll_promise = navigable->perform_a_scroll_of_an_element(*this, scroll_offset, options.behavior, relative_displacement); if (promise) WebIDL::resolve_promise(*promise, scroll_promise->promise()); (void)scroll_promise; @@ -5125,7 +5129,8 @@ void Element::scroll_by(ScrollToOptions options, GC::Ptr promis options.top = scroll_top() + top; // 5. Return the Promise returned by scroll() after the method is invoked with options as the only argument. - scroll(options, promise); + CSSPixelPoint relative_displacement { CSSPixels::nearest_value_for(left), CSSPixels::nearest_value_for(top) }; + scroll(options, promise, relative_displacement); } // https://drafts.csswg.org/cssom-view-1/#dom-element-checkvisibility diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index 7acdcaa11b2ac..9ef52de777a5c 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -673,7 +673,7 @@ class WEB_API Element using ScrollToOptions = Bindings::ScrollToOptions; - void scroll(ScrollToOptions, GC::Ptr); + void scroll(ScrollToOptions, GC::Ptr, Optional relative_displacement = {}); void scroll(double x, double y, GC::Ptr); void scroll_by(ScrollToOptions, GC::Ptr); void scroll_by(double x, double y, GC::Ptr); diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 0dbb8f6930823..1ea633aff8c50 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -37,6 +37,8 @@ class XMLDocumentBuilder; enum class InvalidateDisplayList; enum class TraversalDecision; +enum class WheelDeltaPrecision : u8; +enum class ScrollGesturePhase : u8; struct AsyncScrollOperation; struct InitiatorSourceSnapshot; @@ -65,6 +67,7 @@ class DisplayList; class DisplayListPlayerSkia; class DisplayListResourceStorage; struct DisplayListResourceSet; +enum class CompositorScrollNodeKind : u8; enum class PaintCommandCacheMode : u8; struct GradientPaintStyle; struct PatternPaintStyle; diff --git a/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index 961832e29b804..a3dc0c3ba01f2 100644 --- a/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp @@ -267,9 +267,9 @@ void EventLoop::process_input_events() const case MouseEvent::Type::MouseWheel: if (mouse_event.async_scroll_performed_default_action) { dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread handling DOM wheel after async default action"); - return page.handle_mousewheel(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.wheel_delta_x, mouse_event.wheel_delta_y, true); + return page.handle_mousewheel(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.wheel_delta_x, mouse_event.wheel_delta_y, mouse_event.wheel_delta_precision, mouse_event.scroll_gesture_phase, true); } - return page.handle_mousewheel(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.wheel_delta_x, mouse_event.wheel_delta_y); + return page.handle_mousewheel(mouse_event.position, mouse_event.screen_position, mouse_event.button, mouse_event.buttons, mouse_event.modifiers, mouse_event.wheel_delta_x, mouse_event.wheel_delta_y, mouse_event.wheel_delta_precision, mouse_event.scroll_gesture_phase); } VERIFY_NOT_REACHED(); }, @@ -430,9 +430,14 @@ void EventLoop::update_the_rendering() // Clamp viewport scroll offset to valid range after layout, in case the // scrollable overflow area has shrunk (e.g. after a viewport size change). - if (auto navigable = document->navigable()) + if (auto navigable = document->navigable()) { navigable->clamp_viewport_scroll_offset(); + // AD-HOC: A user scroll gesture that ended while layout was out of date could not select the snap + // position it ends at, so it does now. + navigable->snap_user_scroll_gestures_that_awaited_layout(); + } + // 2. Let hadInitialVisibleContentVisibilityDetermination be false. bool had_initial_visible_content_visibility_determination = false; diff --git a/Libraries/LibWeb/HTML/LocalNavigable.cpp b/Libraries/LibWeb/HTML/LocalNavigable.cpp index 91f0148378e73..8b9cc42347e46 100644 --- a/Libraries/LibWeb/HTML/LocalNavigable.cpp +++ b/Libraries/LibWeb/HTML/LocalNavigable.cpp @@ -6,7 +6,10 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include #include +#include #include #include #include @@ -79,6 +82,7 @@ #include #include #include +#include #include #include #include @@ -714,11 +718,11 @@ void LocalNavigable::visit_edges(Cell::Visitor& visitor) } for (auto& async_scroll_operation : m_pending_async_scroll_operations) - visitor.visit(async_scroll_operation.promise); + visitor.visit(async_scroll_operation.promises); for (auto& smooth_scroll : m_main_thread_smooth_scrolls) - visitor.visit(smooth_scroll.promise); - for (auto& target : m_pending_user_scrollend_targets) - visitor.visit(target); + visitor.visit(smooth_scroll.promises); + for (auto& entry : m_pending_user_scrollend_targets) + visitor.visit(entry.target); } void LocalNavigable::NavigateParams::visit_edges(Cell::Visitor& visitor) @@ -3827,6 +3831,30 @@ static void queue_async_scroll_operation_promise_resolution(GC::Ref stable_node_id, ScrollTrigger trigger, Optional scroll_offset_before_scroll, ScrollPromises const& promises) +{ + if (stable_node_id.has_value() && scroll_offset_before_scroll.has_value()) { + auto final_scroll_offset = scroll_offset_for(*stable_node_id); + if (final_scroll_offset.has_value() && *final_scroll_offset != *scroll_offset_before_scroll) + queue_scrollend_event_for_finished_scroll(*stable_node_id, trigger, scroll_offset_before_scroll); + } + for (auto const& promise : promises) + queue_async_scroll_operation_promise_resolution(promise); +} + +LocalNavigable::ScrollPromises* LocalNavigable::promises_of_smooth_scroll_in_flight_toward(Compositor::AsyncScrollNodeStableID stable_node_id, CSSPixelPoint position, ScrollTrigger trigger) +{ + for (auto& pending : m_pending_async_scroll_operations) { + if (pending.stable_node_id == stable_node_id && pending.destination_scroll_offset == position && pending.trigger == trigger) + return &pending.promises; + } + for (auto& smooth_scroll : m_main_thread_smooth_scrolls) { + if (smooth_scroll.stable_node_id == stable_node_id && smooth_scroll.destination_scroll_offset == position && smooth_scroll.trigger == trigger) + return &smooth_scroll.promises; + } + return nullptr; +} + void LocalNavigable::wait_for_async_scroll_operation(Compositor::AsyncScrollOperationID operation_id, GC::Ref promise) { if (has_been_destroyed() || !all_local_navigables().contains(*this)) { @@ -3836,47 +3864,51 @@ void LocalNavigable::wait_for_async_scroll_operation(Compositor::AsyncScrollOper m_pending_async_scroll_operations.append(PendingAsyncScrollOperation { .operation_id = operation_id, - .promise = promise, + .promises = { promise }, .stable_node_id = {}, .initial_scroll_offset = {}, + .destination_scroll_offset = {}, }); } -void LocalNavigable::resolve_async_scroll_operation(Compositor::AsyncScrollOperationID operation_id) +void LocalNavigable::resolve_async_scroll_operation(Compositor::AsyncScrollOperationID operation_id, AsyncScrollCompletion completion) { + // Notifying a scroll's completion can start the next scroll of the same scrolling box, so the finished scroll + // leaves the list of scrolls in progress before it is reported. + Optional finished; m_pending_async_scroll_operations.remove_first_matching([&](auto const& pending) { if (pending.operation_id != operation_id) return false; - - if (pending.stable_node_id.has_value() && pending.initial_scroll_offset.has_value()) { - auto final_scroll_offset = scroll_offset_for(*pending.stable_node_id); - if (final_scroll_offset.has_value() && *final_scroll_offset != *pending.initial_scroll_offset) - queue_scrollend_event_for_finished_scroll(*pending.stable_node_id, pending.trigger); - } - queue_async_scroll_operation_promise_resolution(pending.promise); + finished = pending; return true; }); + if (!finished.has_value()) + return; + + // A scroll that user input took over belongs to the gesture that input continues, which reports the end of the + // combined scrolling operation; reporting no offset to have scrolled from resolves such a scroll's promise + // without queueing an event of its own. + auto scroll_offset_the_finished_scroll_reports = completion == AsyncScrollCompletion::Finished + ? finished->initial_scroll_offset + : Optional {}; + + queue_scrollend_event_and_promise_resolution_for_finished_scroll(finished->stable_node_id, finished->trigger, scroll_offset_the_finished_scroll_reports, finished->promises); + settle_user_scroll_gesture_if_input_deadline_passed(); } void LocalNavigable::resolve_all_pending_async_scroll_operations() { while (!m_pending_async_scroll_operations.is_empty()) { auto pending = m_pending_async_scroll_operations.take_last(); - if (pending.stable_node_id.has_value() && pending.initial_scroll_offset.has_value()) { - auto final_scroll_offset = scroll_offset_for(*pending.stable_node_id); - if (final_scroll_offset.has_value() && *final_scroll_offset != *pending.initial_scroll_offset) - queue_scrollend_event_for_finished_scroll(*pending.stable_node_id, pending.trigger); - } - queue_async_scroll_operation_promise_resolution(pending.promise); + queue_scrollend_event_and_promise_resolution_for_finished_scroll(pending.stable_node_id, pending.trigger, pending.initial_scroll_offset, pending.promises); } while (!m_main_thread_smooth_scrolls.is_empty()) { auto smooth_scroll = m_main_thread_smooth_scrolls.take_last(); - auto final_scroll_offset = scroll_offset_for(smooth_scroll.stable_node_id); - if (final_scroll_offset.has_value() && *final_scroll_offset != smooth_scroll.initial_scroll_offset) - queue_scrollend_event_for_finished_scroll(smooth_scroll.stable_node_id, smooth_scroll.trigger); - queue_async_scroll_operation_promise_resolution(smooth_scroll.promise); + queue_scrollend_event_and_promise_resolution_for_finished_scroll(smooth_scroll.stable_node_id, smooth_scroll.trigger, smooth_scroll.initial_scroll_offset, smooth_scroll.promises); } + + settle_user_scroll_gesture_if_input_deadline_passed(); } Optional LocalNavigable::scroll_offset_for(Compositor::AsyncScrollNodeStableID stable_node_id) const @@ -3897,6 +3929,26 @@ Optional LocalNavigable::scroll_offset_for(Compositor::AsyncScrol return element->scroll_offset(pseudo_element_from_async_scroll_node_stable_id(stable_node_id)); } +static RefPtr paintable_for_async_scroll_node(DOM::Document& document, Compositor::AsyncScrollNodeStableID stable_node_id) +{ + if (stable_node_id.kind == Compositor::AsyncScrollNodeKind::Viewport) { + if (stable_node_id.node_id != document.unique_id()) + return nullptr; + return document.paintable_box(); + } + + auto* element = element_for_async_scroll_node_stable_id(document, stable_node_id); + if (!element) + return nullptr; + if (auto pseudo_element = pseudo_element_from_async_scroll_node_stable_id(stable_node_id); pseudo_element.has_value()) { + auto synthetic_pseudo_element = element->get_synthetic_pseudo_element(*pseudo_element); + if (!synthetic_pseudo_element.has_value() || !synthetic_pseudo_element->layout_node()) + return nullptr; + return synthetic_pseudo_element->layout_node()->paintable(); + } + return element->paintable_box(); +} + bool LocalNavigable::set_scroll_offset_for(Compositor::AsyncScrollNodeStableID stable_node_id, CSSPixelPoint scroll_offset) { auto document = active_document(); @@ -3911,25 +3963,31 @@ bool LocalNavigable::set_scroll_offset_for(Compositor::AsyncScrollNodeStableID s return old_scroll_offset != m_viewport_scroll_offset; } - auto* element = element_for_async_scroll_node_stable_id(*document, stable_node_id); - if (!element) + if (!element_for_async_scroll_node_stable_id(*document, stable_node_id)) return false; document->update_layout(DOM::UpdateLayoutReason::ElementScroll); - Optional pseudo_element = pseudo_element_from_async_scroll_node_stable_id(stable_node_id); - RefPtr paintable; - if (pseudo_element.has_value()) { - auto synthetic_pseudo_element = element->get_synthetic_pseudo_element(*pseudo_element); - if (!synthetic_pseudo_element.has_value() || !synthetic_pseudo_element->layout_node()) - return false; - paintable = synthetic_pseudo_element->layout_node()->paintable(); - } else { - paintable = element->paintable_box(); - } + auto paintable = paintable_for_async_scroll_node(*document, stable_node_id); if (!paintable) return false; return paintable->set_scroll_offset(scroll_offset) == Painting::Paintable::ScrollHandled::Yes; } +static void record_snapped_areas_of_scroll_container(DOM::Document& document, Compositor::AsyncScrollNodeStableID stable_node_id, Painting::SnapDestination& snap_destination) +{ + // https://drafts.csswg.org/css-scroll-snap-1/#re-snap + // If the scroll container was snapped before the content change and those same snap areas still exist (e.g. their + // associated elements were not deleted), the scroll container must be re-snapped to those same snap areas after the + // content change. + + // NB: An axis the selection did not evaluate keeps the snap areas it is already snapped to. + Painting::SnappedAreas snapped_areas = document.snapped_areas_of_scroll_container(stable_node_id); + if (snap_destination.evaluated_x) + snapped_areas.x = move(snap_destination.snapped_areas.x); + if (snap_destination.evaluated_y) + snapped_areas.y = move(snap_destination.snapped_areas.y); + document.set_snapped_areas_of_scroll_container(stable_node_id, move(snapped_areas)); +} + static GC::Ptr scroll_event_target_for_async_scroll_node(DOM::Document& document, Compositor::AsyncScrollNodeStableID stable_node_id) { if (stable_node_id.kind == Compositor::AsyncScrollNodeKind::Viewport) { @@ -3940,7 +3998,19 @@ static GC::Ptr scroll_event_target_for_async_scroll_node(DOM:: return element_for_async_scroll_node_stable_id(document, stable_node_id); } -void LocalNavigable::queue_scrollend_event(Compositor::AsyncScrollNodeStableID stable_node_id, ScrollTrigger trigger) +LocalNavigable::PendingUserScrollendTarget* LocalNavigable::latched_user_scroll_gesture_for(GC::Ref target, Optional const& stable_node_id) +{ + auto index = m_pending_user_scrollend_targets.find_first_index_if([&](auto const& entry) { + if (stable_node_id.has_value() && entry.stable_node_id.has_value()) + return *entry.stable_node_id == *stable_node_id; + return entry.target.ptr() == target.ptr(); + }); + if (!index.has_value()) + return nullptr; + return &m_pending_user_scrollend_targets[*index]; +} + +void LocalNavigable::queue_scrollend_event(Compositor::AsyncScrollNodeStableID stable_node_id, ScrollTrigger trigger, Optional scroll_offset_before_scroll) { auto document = active_document(); if (!document) @@ -3950,49 +4020,58 @@ void LocalNavigable::queue_scrollend_event(Compositor::AsyncScrollNodeStableID s if (!target) return; - queue_scrollend_event(*document, *target, trigger); + queue_scrollend_event(*document, *target, stable_node_id, trigger, scroll_offset_before_scroll); } -void LocalNavigable::queue_scrollend_event(DOM::Document& document, GC::Ref target, ScrollTrigger trigger) +void LocalNavigable::queue_scrollend_event(DOM::Document& document, GC::Ref target, Optional stable_node_id, ScrollTrigger trigger, Optional scroll_offset_before_scroll) { if (trigger == ScrollTrigger::UserInput) - queue_scrollend_event_after_user_scroll(target); + queue_scrollend_event_after_user_scroll(target, stable_node_id, scroll_offset_before_scroll); else document.append_pending_scroll_event({ target, EventNames::scrollend }); } -void LocalNavigable::queue_scrollend_event_for_finished_scroll(Compositor::AsyncScrollNodeStableID stable_node_id, ScrollTrigger trigger) +void LocalNavigable::queue_scrollend_event_for_finished_scroll(Compositor::AsyncScrollNodeStableID stable_node_id, ScrollTrigger trigger, Optional scroll_offset_before_scroll) { - // Position updates for the scrolling box are finished, so once no held input remains, both completion conditions - // for the scroll are met and its scrollend event is queued immediately. - if (trigger == ScrollTrigger::UserInput && m_user_scroll_gesture_hold_count == 0) { - auto document = active_document(); - if (!document) - return; - auto target = scroll_event_target_for_async_scroll_node(*document, stable_node_id); - if (!target) - return; - m_pending_user_scrollend_targets.remove_first_matching([&](auto const& pending_target) { return pending_target.ptr() == target.ptr(); }); - if (m_pending_user_scrollend_targets.is_empty()) { - if (m_user_scroll_settle_timer) - m_user_scroll_settle_timer->stop(); - } else if (m_user_scroll_settle_timer && !m_user_scroll_settle_timer->is_active()) { - m_user_scroll_settle_timer->restart(); - } + auto document = active_document(); + if (!document) + return; + auto target = scroll_event_target_for_async_scroll_node(*document, stable_node_id); + if (!target) + return; + + if (trigger != ScrollTrigger::UserInput) { document->append_pending_scroll_event({ *target, EventNames::scrollend }); return; } - queue_scrollend_event(stable_node_id, trigger); + + if (latched_user_scroll_gesture_for(*target, stable_node_id)) + return; + + if (m_user_scroll_gesture_hold_count > 0) { + queue_scrollend_event_after_user_scroll(*target, stable_node_id, scroll_offset_before_scroll); + return; + } + + document->append_pending_scroll_event({ *target, EventNames::scrollend }); } -void LocalNavigable::queue_scrollend_event_after_user_scroll(GC::Ref target) +void LocalNavigable::queue_scrollend_event_after_user_scroll(GC::Ref target, Optional stable_node_id, Optional scroll_offset_before_scroll, SnapPositionSelection snap_position_selection) { // AD-HOC: Wheel events carry no gesture phase information, so a scroll gesture is considered finished once no // user scrolling has moved this navigable's scrolling boxes for 500 milliseconds. static constexpr int user_scroll_settle_delay_ms = 500; - if (!m_pending_user_scrollend_targets.contains_slow(target)) - m_pending_user_scrollend_targets.append(target); + if (auto* existing_entry = latched_user_scroll_gesture_for(target, stable_node_id)) { + if (!existing_entry->scroll_offset_at_gesture_start.has_value()) + existing_entry->scroll_offset_at_gesture_start = scroll_offset_before_scroll; + existing_entry->intent = m_user_scroll_input_intent; + existing_entry->travels_under_momentum = m_user_scroll_gesture_travels_under_momentum; + existing_entry->snap_position_selection = snap_position_selection; + existing_entry->awaits_layout_for_snapping = false; + } else { + m_pending_user_scrollend_targets.append({ target, stable_node_id, scroll_offset_before_scroll, {}, m_user_scroll_input_intent, m_user_scroll_gesture_travels_under_momentum, snap_position_selection }); + } if (!m_user_scroll_settle_timer) { m_user_scroll_settle_timer = Core::Timer::create_single_shot(user_scroll_settle_delay_ms, [this] { @@ -4002,6 +4081,11 @@ void LocalNavigable::queue_scrollend_event_after_user_scroll(GC::Refrestart(); } +void LocalNavigable::note_user_scroll_input_intent(Painting::SnapSelectionStrategy::Type intent) +{ + m_user_scroll_input_intent = intent; +} + void LocalNavigable::defer_user_scroll_settlement() { // User input activity postpones settlement of already latched targets, but never latches new ones, so scrolling @@ -4011,11 +4095,168 @@ void LocalNavigable::defer_user_scroll_settlement() m_user_scroll_settle_timer->restart(); } +void LocalNavigable::note_user_scroll_gesture_phase(ScrollGesturePhase phase) +{ + switch (phase) { + case ScrollGesturePhase::None: + m_user_scroll_gesture_travels_under_momentum = false; + reset_momentum_fling_state(); + m_wheel_user_scroll_gesture_hold = nullptr; + break; + case ScrollGesturePhase::Ongoing: + case ScrollGesturePhase::Momentum: { + bool travels_under_momentum = phase == ScrollGesturePhase::Momentum; + + if (!travels_under_momentum) + reset_momentum_fling_state(); + + if (m_wheel_user_scroll_gesture_hold && m_user_scroll_gesture_travels_under_momentum != travels_under_momentum) + m_wheel_user_scroll_gesture_hold = nullptr; + m_user_scroll_gesture_travels_under_momentum = travels_under_momentum; + + if (!m_wheel_user_scroll_gesture_hold) + m_wheel_user_scroll_gesture_hold = make(*this); + break; + } + case ScrollGesturePhase::Ended: + m_user_scroll_gesture_travels_under_momentum = false; + reset_momentum_fling_state(); + if (m_wheel_user_scroll_gesture_hold) { + m_wheel_user_scroll_gesture_hold = nullptr; + break; + } + settle_user_scroll_gesture(); + break; + } +} + +void LocalNavigable::reset_momentum_fling_state() +{ + m_momentum_snap_position_selection = MomentumSnapPositionSelection::NotSelectedYet; + m_momentum_fling_estimator.reset(); +} + +void LocalNavigable::settle_user_scroll_gesture() +{ + if (m_pending_user_scrollend_targets.is_empty()) + return; + + if (m_user_scroll_gesture_hold_count > 0) + return; + + m_user_scroll_settle_timer->stop(); + user_scroll_did_settle(); +} + +void LocalNavigable::snap_user_scroll_gestures_that_awaited_layout() +{ + if (!any_of(m_pending_user_scrollend_targets, [](auto const& entry) { return entry.awaits_layout_for_snapping; })) + return; + user_scroll_did_settle(UserScrollSettlement::SnappingDeferredUntilLayout); +} + +// https://drafts.csswg.org/css-scroll-snap-1/#re-snap +void LocalNavigable::re_snap_scroll_containers_after_layout_change() +{ + // If the content or layout of the document changes (e.g. content is added, moved, deleted, resized) such that the + // content of a snapport changes, the UA must re-evaluate the resulting scroll position, and re-snap if required. + + if (m_is_re_snapping_scroll_containers) + return; + + auto document = active_document(); + if (!document || !document->needs_scroll_container_resnap()) + return; + + if (!document->may_have_scroll_snap_areas()) { + document->cancel_scheduled_scroll_container_resnap(); + return; + } + + // NB: Not every completed layout update leaves usable layout behind, such as one for a document created for + // template contents; re-snapping then waits for an update that does. + if (!document->layout_is_up_to_date() || document->is_running_update_layout()) + return; + + auto viewport_paintable = document->paintable(); + if (!viewport_paintable) + return; + + if (m_user_scroll_gesture_hold_count > 0) + return; + + document->cancel_scheduled_scroll_container_resnap(); + TemporaryChange re_snapping_in_progress { m_is_re_snapping_scroll_containers, true }; + + auto snap_containers = document->collect_scroll_snap_containers(); + + bool any_snap_container_deferred = false; + for (auto const& snap_container : snap_containers) { + auto stable_node_id = snap_container->async_scroll_node_stable_id(); + if (!stable_node_id.has_value()) + continue; + + // A scrolling box that is being scrolled re-snaps once that scroll settles from wherever it comes to rest, + // rather than having the position it is moving toward re-evaluated out from under it. + auto target = scroll_event_target_for_async_scroll_node(*document, *stable_node_id); + bool has_latched_gesture = target && latched_user_scroll_gesture_for(*target, stable_node_id); + if (has_latched_gesture || in_flight_scroll_for(*stable_node_id).has_value()) { + any_snap_container_deferred = true; + continue; + } + + auto current_scroll_offset = scroll_offset_for(*stable_node_id); + if (!current_scroll_offset.has_value()) + continue; + + auto const& snapped_areas = document->snapped_areas_of_scroll_container(*stable_node_id); + Painting::ResnapSelection resnap_selection { + .snapped_areas = snapped_areas, + .focused_node = document->focused_area(), + .targeted_element = document->target_element(), + }; + auto snap_destination = Painting::select_resnap_destination(*snap_container, *current_scroll_offset, resnap_selection); + + // Scrolling behavior for re-snapping to the same box as before however, is UA-defined. The UA may, for + // example, when snapped to the start of a section, choose not to animate the scroll to the section's new + // position as content is dynamically added earlier in the document in order to create the illusion of not + // scrolling. + // NB: Re-snapping to snap areas the container was already snapped to is therefore instant. + auto is_subset_of = [](Vector const& areas, Vector const& other_areas) { + return all_of(areas, [&](auto const& area) { return other_areas.contains_slow(area); }); + }; + bool re_snapped_to_same_areas = !snap_destination.snapped_areas.is_empty() + && is_subset_of(snap_destination.snapped_areas.x, snapped_areas.x) + && is_subset_of(snap_destination.snapped_areas.y, snapped_areas.y); + + document->set_snapped_areas_of_scroll_container(*stable_node_id, move(snap_destination.snapped_areas)); + + if (snap_destination.position == *current_scroll_offset) + continue; + + // Scrolling required by a re-snap operation to a new or different box must behave and animate the same way as + // any other scroll-into-view operation, including honoring controls such as scroll-behavior. + auto behavior = re_snapped_to_same_areas ? Bindings::ScrollBehavior::Instant : Bindings::ScrollBehavior::Auto; + GC::Ptr associated_element = stable_node_id->kind == Compositor::AsyncScrollNodeKind::Viewport + ? document->document_element() + : element_for_async_scroll_node_stable_id(*document, *stable_node_id); + + TemporaryExecutionContext temporary_execution_context { HTML::relevant_realm(*document) }; + perform_a_scroll_of_a_scrolling_box(*stable_node_id, snap_destination.position, behavior, associated_element, ScrollTrigger::Programmatic, {}, DestinationSnapping::DestinationIsSnapPosition); + } + + // A deferred snap container re-snaps once the scroll that owns it completes and settlement runs this again. + if (any_snap_container_deferred) + document->schedule_scroll_container_resnap(); +} + void LocalNavigable::cancel_user_scroll_settlement() { if (m_user_scroll_settle_timer) m_user_scroll_settle_timer->stop(); m_pending_user_scrollend_targets.clear(); + m_compositor_user_scroll_gesture_hold = nullptr; + m_wheel_user_scroll_gesture_hold = nullptr; } void LocalNavigable::begin_user_scroll_gesture_hold(Badge) @@ -4028,31 +4269,72 @@ void LocalNavigable::end_user_scroll_gesture_hold(Badge) VERIFY(m_user_scroll_gesture_hold_count > 0); if (--m_user_scroll_gesture_hold_count > 0) return; - if (m_pending_user_scrollend_targets.is_empty()) - return; - - if (has_in_flight_user_scroll_operation()) - return; // The release of the last held input completes the scroll gesture. - m_user_scroll_settle_timer->stop(); - user_scroll_did_settle(); + settle_user_scroll_gesture(); } -bool LocalNavigable::has_in_flight_user_scroll_operation() const +Optional LocalNavigable::in_flight_scroll_for(Optional const& stable_node_id) const { + if (!stable_node_id.has_value()) + return {}; + + Optional in_flight_scroll; + auto consider = [&](ScrollTrigger trigger, Optional destination_scroll_offset) { + if (in_flight_scroll.has_value() && in_flight_scroll->trigger == ScrollTrigger::UserInput) + return; + in_flight_scroll = InFlightScroll { trigger, destination_scroll_offset }; + }; for (auto const& pending : m_pending_async_scroll_operations) { - if (pending.trigger == ScrollTrigger::UserInput) - return true; + if (pending.stable_node_id == stable_node_id) + consider(pending.trigger, pending.destination_scroll_offset); } for (auto const& smooth_scroll : m_main_thread_smooth_scrolls) { - if (smooth_scroll.trigger == ScrollTrigger::UserInput) - return true; + if (smooth_scroll.stable_node_id == *stable_node_id) + consider(smooth_scroll.trigger, smooth_scroll.destination_scroll_offset); } - return false; + return in_flight_scroll; +} + +void LocalNavigable::abandon_snapping_of_user_scroll_gesture(Compositor::AsyncScrollNodeStableID stable_node_id) +{ + auto document = active_document(); + if (!document) + return; + auto target = scroll_event_target_for_async_scroll_node(*document, stable_node_id); + if (!target) + return; + + auto* entry = latched_user_scroll_gesture_for(*target, stable_node_id); + if (!entry) + return; + + // The entry remains only to deliver the scrollend event the gesture owes. + entry->scroll_offset_at_gesture_start = {}; + entry->unsnapped_scroll_destination = {}; + entry->intent = Painting::SnapSelectionStrategy::Type::EndPosition; + entry->snap_position_selection = SnapPositionSelection::AtGestureEnd; } -void LocalNavigable::user_scroll_did_settle() +void LocalNavigable::settle_user_scroll_gesture_if_input_deadline_passed() +{ + if (m_pending_user_scrollend_targets.is_empty()) + return; + if (m_user_scroll_settle_timer && m_user_scroll_settle_timer->is_active()) + return; + + // Starting a scroll of a scrolling box aborts the scrolls already running for it, and settling in the middle of + // that would enqueue a snap scroll of the same box alongside the one being started. The scroll that is starting + // settles the gesture once it is under way instead. + if (m_scrolls_being_started > 0) { + m_user_scroll_settlement_awaits_scroll_start = true; + return; + } + + user_scroll_did_settle(); +} + +void LocalNavigable::user_scroll_did_settle(UserScrollSettlement settlement) { if (has_been_destroyed()) return; @@ -4066,8 +4348,20 @@ void LocalNavigable::user_scroll_did_settle() if (!document) return; + // Settlement can occur inside a layout update when tearing down a scrollbar whose thumb is grabbed, so snapping + // geometry is only consulted when layout is already up to date. + bool can_snap = document->layout_is_up_to_date() && !document->is_running_update_layout(); + bool queued_any_scrollend_event = false; - for (auto const& target : targets) { + for (auto& entry : targets) { + // A settlement performed once layout is up to date is only for the gestures an earlier one left waiting for + // it; the rest are still waiting for their own input to run out. + if (settlement == UserScrollSettlement::SnappingDeferredUntilLayout && !entry.awaits_layout_for_snapping) { + m_pending_user_scrollend_targets.append(move(entry)); + continue; + } + + auto const& target = entry.target; if (auto* element = as_if(*target)) { if (&element->document() != document.ptr() || !element->is_connected()) continue; @@ -4075,6 +4369,57 @@ void LocalNavigable::user_scroll_did_settle() continue; } + auto const& stable_node_id = entry.stable_node_id; + auto in_flight_scroll_trigger = in_flight_scroll_for(stable_node_id).map([](auto const& in_flight_scroll) { return in_flight_scroll.trigger; }); + + // A user scroll that is still running has not reached the position the gesture ends at. The completion of that + // scroll settles the gesture instead. + if (in_flight_scroll_trigger == ScrollTrigger::UserInput) { + m_pending_user_scrollend_targets.append(move(entry)); + continue; + } + + // https://drafts.csswg.org/css-scroll-snap-1/#snap-strictness + // If a valid snap position exists then the scroll container must snap at the termination of a scroll (if none + // exist then no snapping occurs). + // NB: A programmatic scroll of the scrolling box decides where it comes to rest, and snapped its own + // destination when it started, so the gesture only delivers the scrollend event it owes. + bool snaps_at_this_settlement = in_flight_scroll_trigger != ScrollTrigger::Programmatic && stable_node_id.has_value(); + + // A gesture whose snap position cannot be selected yet keeps the scrolling box latched and snaps once layout + // is up to date. A settlement that already waited for layout once takes what it can get, so that a scrolling + // box cannot stay latched. + if (snaps_at_this_settlement && !can_snap && !entry.awaits_layout_for_snapping) { + entry.awaits_layout_for_snapping = true; + m_pending_user_scrollend_targets.append(move(entry)); + main_thread_event_loop().queue_task_to_update_the_rendering(); + continue; + } + + if (snaps_at_this_settlement && can_snap) { + auto snap_container = paintable_for_async_scroll_node(*document, *stable_node_id); + auto current_scroll_offset = scroll_offset_for(*stable_node_id); + if (snap_container && current_scroll_offset.has_value()) { + Painting::SnapSelectionStrategy strategy; + if (entry.scroll_offset_at_gesture_start.has_value() && entry.snap_position_selection == SnapPositionSelection::AtGestureEnd) { + strategy.displacement = *current_scroll_offset - *entry.scroll_offset_at_gesture_start; + if (!strategy.displacement.is_zero()) { + strategy.type = entry.intent; + if (entry.intent != Painting::SnapSelectionStrategy::Type::EndPosition || entry.travels_under_momentum) + strategy.start_offset = *entry.scroll_offset_at_gesture_start; + } + } + auto snap_destination = Painting::adjust_scroll_destination_for_snapping(*snap_container, *current_scroll_offset, strategy); + record_snapped_areas_of_scroll_container(*document, *stable_node_id, snap_destination); + if (snap_destination.position != *current_scroll_offset) { + // The snap animation queues this target's scrollend event once it completes. + TemporaryExecutionContext temporary_execution_context { HTML::relevant_realm(*document) }; + perform_a_scroll_of_a_scrolling_box(*stable_node_id, snap_destination.position, Bindings::ScrollBehavior::Smooth, nullptr, ScrollTrigger::UserInput); + continue; + } + } + } + if (document->append_pending_scroll_event({ target, EventNames::scrollend })) queued_any_scrollend_event = true; } @@ -4083,44 +4428,50 @@ void LocalNavigable::user_scroll_did_settle() main_thread_event_loop().queue_task_to_update_the_rendering(); } -void LocalNavigable::resolve_pending_smooth_scrolls(Compositor::AsyncScrollNodeStableID stable_node_id) +void LocalNavigable::resolve_pending_smooth_scrolls(Compositor::AsyncScrollNodeStableID stable_node_id, SmoothScrollAbortCause abort_cause) { - for (size_t index = 0; index < m_pending_async_scroll_operations.size();) { - auto const& pending = m_pending_async_scroll_operations[index]; - if (pending.stable_node_id != stable_node_id) { - ++index; - continue; - } - if (pending.initial_scroll_offset.has_value()) { - auto final_scroll_offset = scroll_offset_for(stable_node_id); - if (final_scroll_offset.has_value() && *final_scroll_offset != *pending.initial_scroll_offset) - queue_scrollend_event_for_finished_scroll(stable_node_id, pending.trigger); - } - queue_async_scroll_operation_promise_resolution(pending.promise); - m_pending_async_scroll_operations.remove(index); - } + Vector finished_async_scroll_operations; + m_pending_async_scroll_operations.remove_all_matching([&](auto const& pending) { + if (pending.stable_node_id != stable_node_id) + return false; + finished_async_scroll_operations.append(pending); + return true; + }); - for (size_t index = 0; index < m_main_thread_smooth_scrolls.size();) { - auto const& smooth_scroll = m_main_thread_smooth_scrolls[index]; - if (smooth_scroll.stable_node_id != stable_node_id) { - ++index; - continue; - } - auto final_scroll_offset = scroll_offset_for(stable_node_id); - if (final_scroll_offset.has_value() && *final_scroll_offset != smooth_scroll.initial_scroll_offset) - queue_scrollend_event_for_finished_scroll(stable_node_id, smooth_scroll.trigger); - queue_async_scroll_operation_promise_resolution(smooth_scroll.promise); - m_main_thread_smooth_scrolls.remove(index); - } + Vector finished_smooth_scrolls; + m_main_thread_smooth_scrolls.remove_all_matching([&](auto const& smooth_scroll) { + if (smooth_scroll.stable_node_id != stable_node_id) + return false; + finished_smooth_scrolls.append(smooth_scroll); + return true; + }); + + // A smooth scroll aborted by a new scroll of the same scrolling box hands the reporting of the scrolling + // operation's end to its replacement; reporting no offset to have scrolled from resolves such a scroll's promise + // without queueing an event of its own. + auto scroll_offset_a_finished_scroll_reports = [&](Optional initial_scroll_offset) { + if (abort_cause == SmoothScrollAbortCause::ReplacedByNewScroll) + return Optional {}; + return initial_scroll_offset; + }; + for (auto const& finished : finished_async_scroll_operations) + queue_scrollend_event_and_promise_resolution_for_finished_scroll(stable_node_id, finished.trigger, scroll_offset_a_finished_scroll_reports(finished.initial_scroll_offset), finished.promises); + for (auto const& finished : finished_smooth_scrolls) + queue_scrollend_event_and_promise_resolution_for_finished_scroll(stable_node_id, finished.trigger, scroll_offset_a_finished_scroll_reports(finished.initial_scroll_offset), finished.promises); + + settle_user_scroll_gesture_if_input_deadline_passed(); } void LocalNavigable::process_main_thread_smooth_scrolls() { auto now = MonotonicTime::now(); + + Vector finished_smooth_scrolls; for (size_t index = 0; index < m_main_thread_smooth_scrolls.size();) { auto& smooth_scroll = m_main_thread_smooth_scrolls[index]; if (!scroll_offset_for(smooth_scroll.stable_node_id).has_value()) { - queue_async_scroll_operation_promise_resolution(smooth_scroll.promise); + for (auto const& promise : smooth_scroll.promises) + queue_async_scroll_operation_promise_resolution(promise); m_main_thread_smooth_scrolls.remove(index); continue; } @@ -4132,16 +4483,19 @@ void LocalNavigable::process_main_thread_smooth_scrolls() auto sample = smooth_scroll.animation.sample(smooth_scroll.elapsed); set_scroll_offset_for(smooth_scroll.stable_node_id, sample.offset.to_type()); if (sample.complete) { - auto final_scroll_offset = scroll_offset_for(smooth_scroll.stable_node_id); - if (final_scroll_offset.has_value() && *final_scroll_offset != smooth_scroll.initial_scroll_offset) - queue_scrollend_event_for_finished_scroll(smooth_scroll.stable_node_id, smooth_scroll.trigger); - queue_async_scroll_operation_promise_resolution(smooth_scroll.promise); - m_main_thread_smooth_scrolls.remove(index); + finished_smooth_scrolls.append(m_main_thread_smooth_scrolls.take(index)); } else { ++index; } } + for (auto const& finished : finished_smooth_scrolls) + queue_scrollend_event_and_promise_resolution_for_finished_scroll(finished.stable_node_id, finished.trigger, finished.initial_scroll_offset, finished.promises); + + // A scroll whose scrolling box went away is dropped above without being reported, so settlement is retried for + // every pass rather than only for the scrolls that ran to their destination. + settle_user_scroll_gesture_if_input_deadline_passed(); + if (!m_main_thread_smooth_scrolls.is_empty()) main_thread_event_loop().queue_task_to_update_the_rendering(); } @@ -4170,6 +4524,19 @@ void LocalNavigable::adopt_pending_async_scroll_offsets() // The compositor process may have already presented newer scroll offsets. Adopt the latest ones before running // rendering-update observers so they see the same scroll positions as the user. auto async_scroll_updates = compositor_context().take_pending_async_scroll_updates(); + + // A gesture that both began and ended since the previous update is held for the length of this one, so that it + // settles here rather than once its input deadline passes. + if ((async_scroll_updates.user_scroll_gesture_in_progress || async_scroll_updates.user_scroll_gesture_ended) + && !m_compositor_user_scroll_gesture_hold) { + m_compositor_user_scroll_gesture_hold = make(*this); + } + + ScopeGuard release_gesture_hold_once_its_scrolls_are_adopted = [&] { + if (!async_scroll_updates.user_scroll_gesture_in_progress) + m_compositor_user_scroll_gesture_hold = nullptr; + }; + if (async_scroll_updates.scroll_offsets.is_empty() && async_scroll_updates.completed_operation_ids.is_empty()) return; @@ -4180,28 +4547,54 @@ void LocalNavigable::adopt_pending_async_scroll_offsets() return; } + // https://drafts.csswg.org/css-scroll-snap-1/#scroll-types + // AD-HOC: The scrolling the compositor process performs on its own is panning and scrollbar thumb dragging, both + // of which report where the user's input came to rest, so their offsets settle as absolute scrolls even + // though the specification lists a panning gesture among the relative scrolls with both an intended + // direction and end position. + if (!async_scroll_updates.scroll_offsets.is_empty()) + note_user_scroll_input_intent(Painting::SnapSelectionStrategy::Type::EndPosition); + + // The compositor process merges the progress of a scroll that user input took over and the delta of that input + // into one offset per scrolling box, so a box that such input scrolled is recognized from the scroll it ended. + auto user_input_took_over_the_scroll_of = [&](Compositor::AsyncScrollNodeStableID stable_node_id) { + return any_of(async_scroll_updates.operation_ids_taken_over_by_user_input, [&](auto operation_id) { + return any_of(m_pending_async_scroll_operations, [&](auto const& pending_operation) { + return pending_operation.operation_id == operation_id && pending_operation.stable_node_id == stable_node_id; + }); + }); + }; + auto device_pixels_per_css_pixel = page().client().device_pixels_per_css_pixel(); bool adopted_any_scroll_offset = false; for (auto const& async_scroll_offset : async_scroll_updates.scroll_offsets) { auto css_scroll_delta = async_scroll_offset_to_css_pixels(async_scroll_offset.unadopted_scroll_delta, device_pixels_per_css_pixel); - bool is_programmatic_smooth_scroll = false; + bool has_in_flight_smooth_scroll = false; for (auto const& pending_operation : m_pending_async_scroll_operations) { if (pending_operation.stable_node_id == async_scroll_offset.stable_node_id) { - is_programmatic_smooth_scroll = true; + has_in_flight_smooth_scroll = true; break; } } - // NB: A programmatic smooth scroll has an absolute destination. Adopt the + // NB: A smooth scroll of this box has an absolute destination. Adopt the // compositor's absolute position so that replacing scroll snapshots // during the animation cannot cause overlapping deltas to accumulate. - if (is_programmatic_smooth_scroll) { + if (has_in_flight_smooth_scroll) { + auto scroll_offset_before_scroll = scroll_offset_for(async_scroll_offset.stable_node_id); auto css_scroll_offset = async_scroll_offset_to_css_pixels(async_scroll_offset.compositor_scroll_offset, device_pixels_per_css_pixel); if (set_scroll_offset_for(async_scroll_offset.stable_node_id, css_scroll_offset)) { adopted_any_scroll_offset = true; dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread adopting async programmatic scroll offset {},{}", async_scroll_offset.compositor_scroll_offset.x(), async_scroll_offset.compositor_scroll_offset.y()); } + + // The gesture of the input that took the scroll over is latched here, so that the scrollend event the + // taken-over scroll owes is delivered once that gesture settles rather than in the middle of it. + if (user_input_took_over_the_scroll_of(async_scroll_offset.stable_node_id)) { + if (auto target = scroll_event_target_for_async_scroll_node(*document, async_scroll_offset.stable_node_id)) + queue_scrollend_event_after_user_scroll(*target, async_scroll_offset.stable_node_id, scroll_offset_before_scroll); + } continue; } @@ -4216,9 +4609,10 @@ void LocalNavigable::adopt_pending_async_scroll_offsets() continue; } + auto scroll_offset_before_scroll = scroll_offset_for(async_scroll_offset.stable_node_id); if (auto element = adopt_async_element_scroll_delta(*document, async_scroll_offset.stable_node_id, css_scroll_delta)) { adopted_any_scroll_offset = true; - queue_scrollend_event_after_user_scroll(*element); + queue_scrollend_event_after_user_scroll(*element, async_scroll_offset.stable_node_id, scroll_offset_before_scroll); dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread adopting async element delta {},{}", async_scroll_offset.unadopted_scroll_delta.x(), async_scroll_offset.unadopted_scroll_delta.y()); } @@ -4227,8 +4621,12 @@ void LocalNavigable::adopt_pending_async_scroll_offsets() if (adopted_any_scroll_offset) schedule_hover_update_after_async_scroll(); - for (auto operation_id : async_scroll_updates.completed_operation_ids) - resolve_async_scroll_operation(operation_id); + for (auto operation_id : async_scroll_updates.completed_operation_ids) { + auto completion = async_scroll_updates.operation_ids_taken_over_by_user_input.contains_slow(operation_id) + ? AsyncScrollCompletion::TakenOverByUserInput + : AsyncScrollCompletion::Finished; + resolve_async_scroll_operation(operation_id, completion); + } } void LocalNavigable::schedule_hover_update_after_async_scroll() @@ -4829,26 +5227,87 @@ void LocalNavigable::render_screenshot(Gfx::PaintingSurface& painting_surface, P compositor_context().request_screenshot(painting_surface, move(callback)); } -GC::Ref LocalNavigable::perform_a_scroll_of_a_scrolling_box(Compositor::AsyncScrollNodeStableID stable_node_id, CSSPixelPoint position, Bindings::ScrollBehavior behavior, GC::Ptr associated_element, ScrollTrigger trigger) +void LocalNavigable::abort_in_flight_smooth_scrolls(Compositor::AsyncScrollNodeStableID stable_node_id, SmoothScrollAbortCause abort_cause) +{ + if (has_compositor_context()) + compositor_context().cancel_smooth_scroll(stable_node_id); + resolve_pending_smooth_scrolls(stable_node_id, abort_cause); +} + +void LocalNavigable::abort_in_flight_smooth_scrolls_taken_over_by_user_input(Compositor::AsyncScrollNodeStableID stable_node_id, CSSPixelPoint scroll_offset_at_gesture_start) +{ + auto document = active_document(); + auto target = document ? scroll_event_target_for_async_scroll_node(*document, stable_node_id) : nullptr; + + // A user scroll that is still running belongs to the gesture this input continues, so that gesture is latched + // before the scroll is taken over from it and the scrollend event the scroll owes is delivered once the gesture + // settles. + auto in_flight_scroll = in_flight_scroll_for(stable_node_id); + if (target && in_flight_scroll.has_value() && in_flight_scroll->trigger == ScrollTrigger::UserInput) { + if (!latched_user_scroll_gesture_for(*target, stable_node_id)) + queue_scrollend_event_after_user_scroll(*target, stable_node_id, scroll_offset_at_gesture_start); + } + + abort_in_flight_smooth_scrolls(stable_node_id, SmoothScrollAbortCause::TakenOverByUserInput); +} + +GC::Ref LocalNavigable::perform_a_scroll_of_a_scrolling_box(Compositor::AsyncScrollNodeStableID stable_node_id, CSSPixelPoint position, Bindings::ScrollBehavior behavior, GC::Ptr associated_element, ScrollTrigger trigger, Optional relative_displacement, DestinationSnapping destination_snapping, Compositor::ScrollAnimationKind animation_kind) { auto document = active_document(); VERIFY(document); + + // A gesture latched for this scrolling box may run out of input while this scroll is being started, so its + // settlement waits until this scroll is under way rather than enqueuing a scroll of its own alongside it. + ++m_scrolls_being_started; + ScopeGuard settle_gesture_that_ran_out_of_input = [this] { + if (--m_scrolls_being_started > 0) + return; + if (!m_user_scroll_settlement_awaits_scroll_start) + return; + m_user_scroll_settlement_awaits_scroll_start = false; + settle_user_scroll_gesture_if_input_deadline_passed(); + }; + auto initial_scroll_offset = scroll_offset_for(stable_node_id); if (!initial_scroll_offset.has_value()) return WebIDL::create_resolved_promise_for(*document, JS::js_undefined()); + // https://drafts.csswg.org/css-scroll-snap-1/#snap-strictness + // If a valid snap position exists then the scroll container must snap at the termination of a scroll (if none + // exist then no snapping occurs). + if (trigger == ScrollTrigger::Programmatic && destination_snapping == DestinationSnapping::SelectSnapPosition) { + abandon_snapping_of_user_scroll_gesture(stable_node_id); + document->update_layout(DOM::UpdateLayoutReason::ElementScroll); + if (auto snap_container = paintable_for_async_scroll_node(*document, stable_node_id)) { + Painting::SnapSelectionStrategy strategy; + if (relative_displacement.has_value() && !relative_displacement->is_zero()) + strategy = { Painting::SnapSelectionStrategy::Type::EndPositionAndDirection, *initial_scroll_offset, *relative_displacement }; + auto snap_destination = Painting::adjust_scroll_destination_for_snapping(*snap_container, position, strategy); + position = snap_destination.position; + record_snapped_areas_of_scroll_container(*document, stable_node_id, snap_destination); + } + } + auto should_scroll_smoothly = behavior == Bindings::ScrollBehavior::Smooth; if (behavior == Bindings::ScrollBehavior::Auto && associated_element) { if (auto const* values = associated_element->style_group()) should_scroll_smoothly = static_cast(values->scroll_behavior) == CSS::ScrollBehavior::Smooth; } + // AD-HOC: A smooth scroll requested while a smooth scroll of the same scrolling box toward the same position is in + // flight continues that scroll instead of restarting it, matching other engines. + if (should_scroll_smoothly) { + if (auto* promises = promises_of_smooth_scroll_in_flight_toward(stable_node_id, position, trigger)) { + auto scroll_promise = WebIDL::create_promise_for(*document); + promises->append(scroll_promise); + return scroll_promise; + } + } + // https://drafts.csswg.org/cssom-view-1/#perform-a-scroll // 1. Abort any ongoing smooth scroll for box. - if (has_compositor_context()) - compositor_context().cancel_smooth_scroll(stable_node_id); // 2. Resolve all pending scroll promises for box. - resolve_pending_smooth_scrolls(stable_node_id); + abort_in_flight_smooth_scrolls(stable_node_id, SmoothScrollAbortCause::ReplacedByNewScroll); // 3. Let scrollPromise be a new promise and return it while the remaining // steps run in parallel. @@ -4860,7 +5319,7 @@ GC::Ref LocalNavigable::perform_a_scroll_of_a_scrolling_box(Com if (!should_scroll_smoothly) { auto did_scroll = set_scroll_offset_for(stable_node_id, position); if (did_scroll) - queue_scrollend_event(stable_node_id, trigger); + queue_scrollend_event(stable_node_id, trigger, initial_scroll_offset); WebIDL::resolve_promise(scroll_promise); return scroll_promise; } @@ -4875,15 +5334,20 @@ GC::Ref LocalNavigable::perform_a_scroll_of_a_scrolling_box(Com static_cast(position.x().to_double() * device_pixels_per_css_pixel), static_cast(position.y().to_double() * device_pixels_per_css_pixel), }; + auto main_thread_offset = Gfx::FloatPoint { + static_cast(initial_scroll_offset->x().to_double() * device_pixels_per_css_pixel), + static_cast(initial_scroll_offset->y().to_double() * device_pixels_per_css_pixel), + }; auto viewport_rect = page().css_to_device_rect(this->viewport_rect()).to_type(); - auto enqueue_result = compositor_context().smooth_scroll_to(stable_node_id, target_offset, viewport_rect, device_pixels_per_css_pixel); + auto enqueue_result = compositor_context().smooth_scroll_to(stable_node_id, target_offset, main_thread_offset, viewport_rect, device_pixels_per_css_pixel, animation_kind); if (enqueue_result.accepted) { VERIFY(enqueue_result.operation_id.has_value()); m_pending_async_scroll_operations.append(PendingAsyncScrollOperation { .operation_id = *enqueue_result.operation_id, - .promise = scroll_promise, + .promises = { scroll_promise }, .stable_node_id = stable_node_id, .initial_scroll_offset = *initial_scroll_offset, + .destination_scroll_offset = position, .trigger = trigger, }); return scroll_promise; @@ -4909,24 +5373,119 @@ GC::Ref LocalNavigable::perform_a_scroll_of_a_scrolling_box(Com } m_main_thread_smooth_scrolls.append(MainThreadSmoothScroll { .stable_node_id = stable_node_id, - .animation = Compositor::SmoothScrollAnimation { initial_scroll_offset->to_type(), position.to_type(), 1.0 }, + .animation = Compositor::SmoothScrollAnimation { initial_scroll_offset->to_type(), position.to_type(), 1.0, animation_kind }, .last_tick = MonotonicTime::now(), .elapsed = AK::Duration::zero(), .initial_scroll_offset = *initial_scroll_offset, - .promise = scroll_promise, + .destination_scroll_offset = position, + .promises = { scroll_promise }, .trigger = trigger, }); main_thread_event_loop().queue_task_to_update_the_rendering(); return scroll_promise; } -GC::Ref LocalNavigable::perform_a_scroll_of_an_element(DOM::Element& element, CSSPixelPoint position, Bindings::ScrollBehavior behavior) +GC::Ref LocalNavigable::perform_a_scroll_of_an_element(DOM::Element& element, CSSPixelPoint position, Bindings::ScrollBehavior behavior, Optional relative_displacement) { return perform_a_scroll_of_a_scrolling_box({ .node_id = element.unique_id(), .kind = Compositor::AsyncScrollNodeKind::Element, }, - position, behavior, element, ScrollTrigger::Programmatic); + position, behavior, element, ScrollTrigger::Programmatic, relative_displacement); +} + +bool LocalNavigable::perform_a_snapped_relative_user_scroll(Painting::Paintable& scroll_container, CSSPixelPoint delta, Painting::SnapSelectionStrategy::Type strategy_type, SnapStepAccumulation step_accumulation, Compositor::ScrollAnimationKind animation_kind) +{ + auto document = active_document(); + if (!document) + return false; + + auto stable_node_id = scroll_container.async_scroll_node_stable_id(); + if (!stable_node_id.has_value()) + return false; + + auto current_scroll_offset = scroll_offset_for(*stable_node_id); + if (!current_scroll_offset.has_value()) + return false; + + auto target = scroll_event_target_for_async_scroll_node(*document, *stable_node_id); + if (!target) + return false; + + // A scroll started for any reason other than user input is going somewhere the gesture never asked for, so a + // gesture's steps then travel from the scrolling box itself instead. + auto in_flight_scroll = in_flight_scroll_for(*stable_node_id); + Optional in_flight_destination; + if (in_flight_scroll.has_value() && in_flight_scroll->trigger == ScrollTrigger::UserInput) + in_flight_destination = in_flight_scroll->destination_scroll_offset; + + // A step selects its snap position from the offset the gesture's input deltas have reached rather than from the + // snap position it is scrolling to, so a burst of steps advances by the distance they asked for instead of by one + // snap position each. + auto* latched_gesture = latched_user_scroll_gesture_for(*target, *stable_node_id); + bool travels_from_input_deltas = latched_gesture + && (step_accumulation == SnapStepAccumulation::UntilGestureSettles || in_flight_destination.has_value()); + auto step_start = travels_from_input_deltas + ? latched_gesture->unsnapped_scroll_destination.value_or(*current_scroll_offset) + : *current_scroll_offset; + auto unsnapped_destination = scroll_container.clamp_scroll_offset(step_start + delta); + + // https://drafts.csswg.org/css-scroll-snap-1/#scroll-types + // NOTE: Scroll snapping responds to a relative scroll by finding the nearest valid snap position in the intended + // direction (if possible), so a snapped element can't get "trapped" when the snap positions are far apart. + Painting::SnapSelectionStrategy strategy { strategy_type, step_start, delta }; + // NB: A step with only an intended direction ignores every snap position up to the offset its input asked for. A + // step with an intended end position selects the snap position nearest that destination, so snap positions + // short of it remain selectable. + if (strategy_type == Painting::SnapSelectionStrategy::Type::Direction) + strategy.starting_positions_boundary = unsnapped_destination; + auto snap_destination = Painting::adjust_scroll_destination_for_snapping(scroll_container, unsnapped_destination, strategy); + + // NB: The step travels only along axes the container selects no snap position in, so it is left to the ordinary + // relative scroll. + if (!(snap_destination.snapped_x && delta.x() != 0) && !(snap_destination.snapped_y && delta.y() != 0)) + return false; + + record_snapped_areas_of_scroll_container(*document, *stable_node_id, snap_destination); + + // NB: A step whose selected snap position is where the scrolling box already rests, or is already scrolling to, is + // consumed without disturbing where it is going. + bool step_rests_at_its_snap_position = snap_destination.position == in_flight_destination.value_or(*current_scroll_offset); + if (!step_rests_at_its_snap_position) + queue_scrollend_event_after_user_scroll(*target, *stable_node_id, *current_scroll_offset, SnapPositionSelection::PerScroll); + + // NB: Latching the gesture above may have moved the entry the offset is recorded on, so it is looked up again. + if (auto* entry = latched_user_scroll_gesture_for(*target, *stable_node_id)) + entry->unsnapped_scroll_destination = unsnapped_destination; + + if (step_rests_at_its_snap_position) + return true; + + TemporaryExecutionContext temporary_execution_context { HTML::relevant_realm(*document) }; + perform_a_scroll_of_a_scrolling_box(*stable_node_id, snap_destination.position, Bindings::ScrollBehavior::Smooth, nullptr, ScrollTrigger::UserInput, {}, DestinationSnapping::SelectSnapPosition, animation_kind); + return true; +} + +// https://drafts.csswg.org/css-scroll-snap-1/#choosing +bool LocalNavigable::perform_a_snapped_momentum_scroll(Painting::Paintable& scroll_container, CSSPixelPoint momentum_delta) +{ + if (m_momentum_snap_position_selection == MomentumSnapPositionSelection::ScrollingToSelectedPosition) + return true; + + if (m_momentum_snap_position_selection == MomentumSnapPositionSelection::NoPositionSelected) + return false; + + auto remaining_displacement = m_momentum_fling_estimator.estimate_remaining_displacement(momentum_delta); + if (!remaining_displacement.has_value()) + return false; + + if (!perform_a_snapped_relative_user_scroll(scroll_container, *remaining_displacement, Painting::SnapSelectionStrategy::Type::EndPositionAndDirection, SnapStepAccumulation::UntilScrollFinishes, Compositor::ScrollAnimationKind::Momentum)) { + m_momentum_snap_position_selection = MomentumSnapPositionSelection::NoPositionSelected; + return false; + } + + m_momentum_snap_position_selection = MomentumSnapPositionSelection::ScrollingToSelectedPosition; + return true; } GC::Ref LocalNavigable::scroll_viewport_by_delta(CSSPixelPoint delta, Bindings::ScrollBehavior behavior) @@ -4937,7 +5496,7 @@ GC::Ref LocalNavigable::scroll_viewport_by_delta(CSSPixelPoint } // https://drafts.csswg.org/cssom-view/#viewport-perform-a-scroll -GC::Ref LocalNavigable::perform_a_scroll_of_the_viewport(CSSPixelPoint position, Bindings::ScrollBehavior behavior, ScrollTrigger trigger) +GC::Ref LocalNavigable::perform_a_scroll_of_the_viewport(CSSPixelPoint position, Bindings::ScrollBehavior behavior, ScrollTrigger trigger, Optional relative_displacement) { // AD-HOC: User input keeps the scroll gesture in progress even when this scroll does not move the viewport, such // as when a held scroll key repeats at the scroll extent. @@ -5000,7 +5559,7 @@ GC::Ref LocalNavigable::perform_a_scroll_of_the_viewport(CSSPix if (visual_delta.is_zero()) doc->set_needs_repaint(Badge {}, InvalidateDisplayList::No); else - queue_scrollend_event(*doc, *vv, trigger); + queue_scrollend_event(*doc, *vv, {}, trigger); // NB: Must update layout before accessing paintables. doc->update_layout(DOM::UpdateLayoutReason::NavigableViewportScroll); @@ -5016,7 +5575,7 @@ GC::Ref LocalNavigable::perform_a_scroll_of_the_viewport(CSSPix .node_id = doc->unique_id(), .kind = Compositor::AsyncScrollNodeKind::Viewport, }, - new_viewport_scroll_offset.to_type(), behavior, doc->document_element(), trigger); + new_viewport_scroll_offset.to_type(), behavior, doc->document_element(), trigger, relative_displacement); // 17. Return scrollPromise, and run the remaining steps in parallel. // 18. Resolve scrollPromise when both scrollPromise1 and scrollPromise2 have settled. diff --git a/Libraries/LibWeb/HTML/LocalNavigable.h b/Libraries/LibWeb/HTML/LocalNavigable.h index fbd84d151406f..45d996db18bef 100644 --- a/Libraries/LibWeb/HTML/LocalNavigable.h +++ b/Libraries/LibWeb/HTML/LocalNavigable.h @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -321,11 +322,45 @@ class WEB_API LocalNavigable : public Navigable { UserInput, }; + // Whether the snap position a gesture ends at is selected as each of its scrolls runs, or once from the offsets + // the whole gesture traveled between. + enum class SnapPositionSelection { + AtGestureEnd, + PerScroll, + }; + + // How long the offset a gesture's input deltas have reached goes on being the offset its next step travels from. + // A wheel gesture stays latched between its steps, so its deltas keep accumulating until it settles, and crossing a + // snap position costs the distance between them however slowly the steps arrive. Keys are separate commands rather + // than one gesture, so each press travels from the scrolling box itself. + enum class SnapStepAccumulation { + UntilScrollFinishes, + UntilGestureSettles, + }; + + enum class SmoothScrollAbortCause { + ReplacedByNewScroll, + TakenOverByUserInput, + }; + + enum class AsyncScrollCompletion { + Finished, + TakenOverByUserInput, + }; + GC::Ref scroll_viewport_by_delta(CSSPixelPoint delta, Bindings::ScrollBehavior = Bindings::ScrollBehavior::Instant); - GC::Ref perform_a_scroll_of_the_viewport(CSSPixelPoint position, Bindings::ScrollBehavior = Bindings::ScrollBehavior::Auto, ScrollTrigger = ScrollTrigger::Programmatic); - GC::Ref perform_a_scroll_of_an_element(DOM::Element&, CSSPixelPoint position, Bindings::ScrollBehavior); - void queue_scrollend_event_after_user_scroll(GC::Ref); + GC::Ref perform_a_scroll_of_the_viewport(CSSPixelPoint position, Bindings::ScrollBehavior = Bindings::ScrollBehavior::Auto, ScrollTrigger = ScrollTrigger::Programmatic, Optional relative_displacement = {}); + GC::Ref perform_a_scroll_of_an_element(DOM::Element&, CSSPixelPoint position, Bindings::ScrollBehavior, Optional relative_displacement = {}); + bool perform_a_snapped_relative_user_scroll(Painting::Paintable&, CSSPixelPoint delta, Painting::SnapSelectionStrategy::Type, SnapStepAccumulation, Compositor::ScrollAnimationKind = Compositor::ScrollAnimationKind::SmoothScroll); + bool perform_a_snapped_momentum_scroll(Painting::Paintable&, CSSPixelPoint momentum_delta); + void re_snap_scroll_containers_after_layout_change(); + void abort_in_flight_smooth_scrolls(Compositor::AsyncScrollNodeStableID, SmoothScrollAbortCause); + void abort_in_flight_smooth_scrolls_taken_over_by_user_input(Compositor::AsyncScrollNodeStableID, CSSPixelPoint scroll_offset_at_gesture_start); + void queue_scrollend_event_after_user_scroll(GC::Ref, Optional, Optional scroll_offset_before_scroll = {}, SnapPositionSelection = SnapPositionSelection::AtGestureEnd); + void note_user_scroll_input_intent(Painting::SnapSelectionStrategy::Type); + void note_user_scroll_gesture_phase(ScrollGesturePhase); void defer_user_scroll_settlement(); + void snap_user_scroll_gestures_that_awaited_layout(); void begin_user_scroll_gesture_hold(Badge); void end_user_scroll_gesture_hold(Badge); void reset_zoom(); @@ -362,17 +397,56 @@ class WEB_API LocalNavigable : public Navigable { void start_download_for_response(GC::Ref, URL::URL const& download_url, ByteString suggested_filename, GC::Ptr); - void resolve_async_scroll_operation(Compositor::AsyncScrollOperationID); + // A request toward the position a scroll is already headed for joins that scroll rather than restarting it, so one + // scroll can owe more than one promise. + using ScrollPromises = Vector, 1>; + + void resolve_async_scroll_operation(Compositor::AsyncScrollOperationID, AsyncScrollCompletion = AsyncScrollCompletion::Finished); void resolve_all_pending_async_scroll_operations(); - void resolve_pending_smooth_scrolls(Compositor::AsyncScrollNodeStableID); - GC::Ref perform_a_scroll_of_a_scrolling_box(Compositor::AsyncScrollNodeStableID, CSSPixelPoint position, Bindings::ScrollBehavior, GC::Ptr associated_element, ScrollTrigger); + void resolve_pending_smooth_scrolls(Compositor::AsyncScrollNodeStableID, SmoothScrollAbortCause); + // Whether a programmatic scroll still needs a snap position selected for its destination, or was given a + // destination that snap position selection already produced. + enum class DestinationSnapping { + SelectSnapPosition, + DestinationIsSnapPosition, + }; + GC::Ref perform_a_scroll_of_a_scrolling_box(Compositor::AsyncScrollNodeStableID, CSSPixelPoint position, Bindings::ScrollBehavior, GC::Ptr associated_element, ScrollTrigger, Optional relative_displacement = {}, DestinationSnapping = DestinationSnapping::SelectSnapPosition, Compositor::ScrollAnimationKind = Compositor::ScrollAnimationKind::SmoothScroll); Optional scroll_offset_for(Compositor::AsyncScrollNodeStableID) const; bool set_scroll_offset_for(Compositor::AsyncScrollNodeStableID, CSSPixelPoint); - void queue_scrollend_event(Compositor::AsyncScrollNodeStableID, ScrollTrigger); - void queue_scrollend_event(DOM::Document&, GC::Ref, ScrollTrigger); - void queue_scrollend_event_for_finished_scroll(Compositor::AsyncScrollNodeStableID, ScrollTrigger); - bool has_in_flight_user_scroll_operation() const; - void user_scroll_did_settle(); + void queue_scrollend_event(Compositor::AsyncScrollNodeStableID, ScrollTrigger, Optional scroll_offset_before_scroll = {}); + void queue_scrollend_event(DOM::Document&, GC::Ref, Optional, ScrollTrigger, Optional scroll_offset_before_scroll = {}); + void queue_scrollend_event_for_finished_scroll(Compositor::AsyncScrollNodeStableID, ScrollTrigger, Optional scroll_offset_before_scroll); + void queue_scrollend_event_and_promise_resolution_for_finished_scroll(Optional, ScrollTrigger, Optional scroll_offset_before_scroll, ScrollPromises const&); + ScrollPromises* promises_of_smooth_scroll_in_flight_toward(Compositor::AsyncScrollNodeStableID, CSSPixelPoint position, ScrollTrigger); + // The scroll a new input to a scrolling box would interact with; a scroll driven by user input is reported over + // any programmatic scroll also in flight. + struct InFlightScroll { + ScrollTrigger trigger { ScrollTrigger::Programmatic }; + Optional destination_scroll_offset; + }; + Optional in_flight_scroll_for(Optional const&) const; + struct PendingUserScrollendTarget { + GC::Ref target; + Optional stable_node_id; + Optional scroll_offset_at_gesture_start; + Optional unsnapped_scroll_destination; + Painting::SnapSelectionStrategy::Type intent { Painting::SnapSelectionStrategy::Type::EndPosition }; + bool travels_under_momentum { false }; + SnapPositionSelection snap_position_selection { SnapPositionSelection::AtGestureEnd }; + bool awaits_layout_for_snapping { false }; + }; + PendingUserScrollendTarget* latched_user_scroll_gesture_for(GC::Ref, Optional const&); + void abandon_snapping_of_user_scroll_gesture(Compositor::AsyncScrollNodeStableID); + void settle_user_scroll_gesture(); + void settle_user_scroll_gesture_if_input_deadline_passed(); + void reset_momentum_fling_state(); + // Which of the latched gestures a settlement is for: every gesture that ran out of input, or only those left + // waiting for layout by an earlier settlement. + enum class UserScrollSettlement { + GestureRanOutOfInput, + SnappingDeferredUntilLayout, + }; + void user_scroll_did_settle(UserScrollSettlement = UserScrollSettlement::GestureRanOutOfInput); void cancel_user_scroll_settlement(); void schedule_hover_update_after_async_scroll(); void update_hover_after_async_scroll_stops(); @@ -447,15 +521,32 @@ class WEB_API LocalNavigable : public Navigable { Painting::DisplayListResourceSet m_compositor_display_list_resources; OwnPtr m_compositor_context; RefPtr m_async_scroll_hover_update_timer; - Vector> m_pending_user_scrollend_targets; + Vector m_pending_user_scrollend_targets; RefPtr m_user_scroll_settle_timer; + OwnPtr m_compositor_user_scroll_gesture_hold; + OwnPtr m_wheel_user_scroll_gesture_hold; size_t m_user_scroll_gesture_hold_count { 0 }; + Painting::SnapSelectionStrategy::Type m_user_scroll_input_intent { Painting::SnapSelectionStrategy::Type::EndPosition }; + bool m_user_scroll_gesture_travels_under_momentum { false }; + // Momentum that selects no snap position is scrolled by for the rest of the gesture rather than being asked again + // for each delta it produces. + enum class MomentumSnapPositionSelection : u8 { + NotSelectedYet, + ScrollingToSelectedPosition, + NoPositionSelected, + }; + MomentumSnapPositionSelection m_momentum_snap_position_selection { MomentumSnapPositionSelection::NotSelectedYet }; + Painting::MomentumFlingEstimator m_momentum_fling_estimator; + size_t m_scrolls_being_started { 0 }; + bool m_user_scroll_settlement_awaits_scroll_start { false }; + bool m_is_re_snapping_scroll_containers { false }; struct PendingAsyncScrollOperation { Compositor::AsyncScrollOperationID operation_id { 0 }; - GC::Ref promise; + ScrollPromises promises; Optional stable_node_id; Optional initial_scroll_offset; + Optional destination_scroll_offset; ScrollTrigger trigger { ScrollTrigger::Programmatic }; }; Vector m_pending_async_scroll_operations; @@ -466,7 +557,8 @@ class WEB_API LocalNavigable : public Navigable { MonotonicTime last_tick; AK::Duration elapsed; CSSPixelPoint initial_scroll_offset; - GC::Ref promise; + CSSPixelPoint destination_scroll_offset; + ScrollPromises promises; ScrollTrigger trigger { ScrollTrigger::Programmatic }; }; Vector m_main_thread_smooth_scrolls; diff --git a/Libraries/LibWeb/HTML/Window.cpp b/Libraries/LibWeb/HTML/Window.cpp index f62bbf2ca90d0..8b4545e13141f 100644 --- a/Libraries/LibWeb/HTML/Window.cpp +++ b/Libraries/LibWeb/HTML/Window.cpp @@ -1729,7 +1729,7 @@ double Window::scroll_y() const } // https://drafts.csswg.org/cssom-view/#dom-window-scroll -void Window::scroll(ScrollToOptions const& options, GC::Ptr promise) +void Window::scroll(ScrollToOptions const& options, GC::Ptr promise, Optional relative_displacement) { // 4. If there is no viewport, return a resolved Promise and abort the remaining steps. // AD-HOC: Done here as step 1 requires the viewport. @@ -1823,13 +1823,13 @@ void Window::scroll(ScrollToOptions const& options, GC::Ptr pro // 12. Perform a scroll of the viewport to position, document’s root element as the associated element, if there is // one, or null otherwise, and the scroll behavior being the value of the behavior dictionary member of options. // Let scrollPromise be the Promise returned from this step. - auto scroll_promise = navigable->perform_a_scroll_of_the_viewport({ x, y }, options.behavior); + auto scroll_promise = navigable->perform_a_scroll_of_the_viewport({ x, y }, options.behavior, LocalNavigable::ScrollTrigger::Programmatic, relative_displacement); if (promise) WebIDL::resolve_promise(*promise, scroll_promise->promise()); } // https://drafts.csswg.org/cssom-view/#dom-window-scroll -void Window::scroll(double x, double y, GC::Ptr promise) +void Window::scroll(double x, double y, GC::Ptr promise, Optional relative_displacement) { // NB: This just implements step 2, and then forwards to the other Window::scroll() overload. @@ -1842,7 +1842,7 @@ void Window::scroll(double x, double y, GC::Ptr promise) options.left = x; options.top = y; - scroll(options, promise); + scroll(options, promise, relative_displacement); } // https://drafts.csswg.org/cssom-view/#dom-window-scrollby @@ -1862,7 +1862,8 @@ void Window::scroll_by(ScrollToOptions options, GC::Ptr promise options.top = top + scroll_y(); // 5. Return the Promise returned from scroll() after the method is invoked with options as the only argument. - scroll(options, promise); + CSSPixelPoint relative_displacement { CSSPixels::nearest_value_for(left), CSSPixels::nearest_value_for(top) }; + scroll(options, promise, relative_displacement); } // https://drafts.csswg.org/cssom-view/#dom-window-scrollby diff --git a/Libraries/LibWeb/HTML/Window.h b/Libraries/LibWeb/HTML/Window.h index 4b373686b153e..8a3dfcccd7430 100644 --- a/Libraries/LibWeb/HTML/Window.h +++ b/Libraries/LibWeb/HTML/Window.h @@ -253,8 +253,8 @@ class WEB_API Window final double scroll_x() const; double scroll_y() const; using ScrollToOptions = Bindings::ScrollToOptions; - void scroll(ScrollToOptions const&, GC::Ptr); - void scroll(double x, double y, GC::Ptr); + void scroll(ScrollToOptions const&, GC::Ptr, Optional relative_displacement = {}); + void scroll(double x, double y, GC::Ptr, Optional relative_displacement = {}); void scroll_by(ScrollToOptions, GC::Ptr); void scroll_by(double x, double y, GC::Ptr); diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index 60f8e7a2b93fb..5c75f47646dc7 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -98,6 +98,26 @@ namespace Web::Internals { static u16 s_echo_server_port { 0 }; +static WheelDeltaPrecision wheel_delta_precision_from(bool precise) +{ + return precise ? WheelDeltaPrecision::Precise : WheelDeltaPrecision::Discrete; +} + +static ScrollGesturePhase scroll_gesture_phase_from(Bindings::ScrollGesturePhase scroll_gesture_phase) +{ + switch (scroll_gesture_phase) { + case Bindings::ScrollGesturePhase::None: + return ScrollGesturePhase::None; + case Bindings::ScrollGesturePhase::Ongoing: + return ScrollGesturePhase::Ongoing; + case Bindings::ScrollGesturePhase::Momentum: + return ScrollGesturePhase::Momentum; + case Bindings::ScrollGesturePhase::Ended: + return ScrollGesturePhase::Ended; + } + VERIFY_NOT_REACHED(); +} + GC_DEFINE_ALLOCATOR(Internals); Internals::Internals(HTML::Window& window) @@ -358,19 +378,26 @@ void Internals::send_text(HTML::HTMLElement& target, Utf16String const& text, We target.focus(); for (auto code_point : text) { - if (auto data = webdriver_key_to_key_code(code_point); data.has_value()) + if (auto data = webdriver_key_to_key_code(code_point); data.has_value()) { page.handle_keydown(data->key_code, modifiers | data->additional_modifiers, data->code_point_to_send, false, data->code_point_to_send != 0); - else + page.handle_keyup(data->key_code, modifiers | data->additional_modifiers, data->code_point_to_send, false); + } else { page.handle_keydown(UIEvents::code_point_to_key_code(code_point), modifiers, code_point, false, true); + page.handle_keyup(UIEvents::code_point_to_key_code(code_point), modifiers, code_point, false); + } } } -void Internals::send_key(HTML::HTMLElement& target, Utf16String const& key_name, WebIDL::UnsignedShort modifiers) +void Internals::send_key(HTML::HTMLElement& target, Utf16String const& key_name, WebIDL::UnsignedShort modifiers, WebIDL::UnsignedLong repeat_count) { + if (repeat_count == 0) + return; + auto key_code = UIEvents::key_code_from_string(key_name.utf16_view()); target.focus(); - page().handle_keydown(key_code, modifiers, 0, false, false); + for (u32 press = 0; press < repeat_count; ++press) + page().handle_keydown(key_code, modifiers, 0, press > 0, false); page().handle_keyup(key_code, modifiers, 0, false); } @@ -449,13 +476,15 @@ void Internals::click_and_hold(double x, double y, WebIDL::UnsignedShort click_c page.handle_mousedown(position, position, mouse_button, 0, modifiers, click_count); } -void Internals::wheel(GC::Ref promise, double x, double y, double delta_x, double delta_y) +void Internals::wheel(GC::Ref promise, double x, double y, double delta_x, double delta_y, bool precise, Bindings::ScrollGesturePhase phase) { auto& page = this->page(); + auto wheel_delta_precision = wheel_delta_precision_from(precise); + auto scroll_gesture_phase = scroll_gesture_phase_from(phase); auto position = page.css_to_device_point({ x, y }); Optional async_scroll_operation; - page.handle_mousewheel(position, position, 0, 0, 0, delta_x, delta_y, false, &async_scroll_operation); + page.handle_mousewheel(position, position, 0, 0, 0, delta_x, delta_y, wheel_delta_precision, scroll_gesture_phase, false, &async_scroll_operation); if (async_scroll_operation.has_value() && async_scroll_operation->navigable) { async_scroll_operation->navigable->wait_for_async_scroll_operation(async_scroll_operation->operation_id, promise); @@ -465,9 +494,9 @@ void Internals::wheel(GC::Ref promise, double x, double y, doub WebIDL::resolve_promise(promise); } -void Internals::wheel(double x, double y, double delta_x, double delta_y, GC::Ref promise) +void Internals::wheel(double x, double y, double delta_x, double delta_y, bool precise, Bindings::ScrollGesturePhase phase, GC::Ref promise) { - wheel(promise, x, y, delta_x, delta_y); + wheel(promise, x, y, delta_x, delta_y, precise, phase); } void Internals::pinch(double x, double y, double scale_delta, WebIDL::UnsignedShort modifiers) @@ -1528,7 +1557,7 @@ Utf16String Internals::async_scrolling_state_wheel_routing_admission() return Utf16String::from_utf16(Compositor::wheel_routing_admission_to_utf16_view(admission)); } -static Compositor::WheelScrollAdmission wheel_scroll_admission_at(DOM::Document& document, double x, double y, double delta_x, double delta_y, bool force_stale_wheel_event_regions) +static Compositor::WheelScrollAdmission wheel_scroll_admission_at(DOM::Document& document, double x, double y, double delta_x, double delta_y, bool precise, bool force_stale_wheel_event_regions) { auto snapshot = capture_async_scrolling_state(document); if (!snapshot.has_value()) @@ -1540,12 +1569,13 @@ static Compositor::WheelScrollAdmission wheel_scroll_admission_at(DOM::Document& snapshot->document_paintable->scroll_state_snapshot(), { static_cast(x), static_cast(y) }, { static_cast(delta_x), static_cast(delta_y) }, + Compositor::snap_container_handling_for(wheel_delta_precision_from(precise), ScrollGesturePhase::None), snapshot->state.has_blocking_wheel_event_listeners && !force_stale_wheel_event_regions); } -bool Internals::async_scrolling_state_can_wheel_scroll_at(double x, double y, double delta_x, double delta_y, bool force_stale_wheel_event_regions) +bool Internals::async_scrolling_state_can_wheel_scroll_at(double x, double y, double delta_x, double delta_y, bool precise, bool force_stale_wheel_event_regions) { - return wheel_scroll_admission_at(window().associated_document(), x, y, delta_x, delta_y, force_stale_wheel_event_regions) == Compositor::WheelScrollAdmission::Accepted; + return wheel_scroll_admission_at(window().associated_document(), x, y, delta_x, delta_y, precise, force_stale_wheel_event_regions) == Compositor::WheelScrollAdmission::Accepted; } static Utf16String wheel_scroll_admission_to_string(Compositor::WheelScrollAdmission admission) @@ -1565,13 +1595,13 @@ static Utf16String wheel_scroll_admission_to_string(Compositor::WheelScrollAdmis VERIFY_NOT_REACHED(); } -Utf16String Internals::async_scrolling_state_wheel_scroll_admission_at(double x, double y, double delta_x, double delta_y, bool force_stale_wheel_event_regions) +Utf16String Internals::async_scrolling_state_wheel_scroll_admission_at(double x, double y, double delta_x, double delta_y, bool precise, bool force_stale_wheel_event_regions) { - auto admission = wheel_scroll_admission_at(window().associated_document(), x, y, delta_x, delta_y, force_stale_wheel_event_regions); + auto admission = wheel_scroll_admission_at(window().associated_document(), x, y, delta_x, delta_y, precise, force_stale_wheel_event_regions); return wheel_scroll_admission_to_string(admission); } -Utf16String Internals::async_scrolling_state_wheel_target_at(double x, double y, double delta_x, double delta_y) +Utf16String Internals::async_scrolling_state_wheel_target_at(double x, double y, double delta_x, double delta_y, bool precise, Bindings::ScrollGesturePhase phase) { auto snapshot = capture_async_scrolling_state(window().associated_document()); if (!snapshot.has_value()) @@ -1583,8 +1613,11 @@ Utf16String Internals::async_scrolling_state_wheel_target_at(double x, double y, auto target = scroll_tree.hit_test_scroll_node_for_wheel( { static_cast(x), static_cast(y) }, - { static_cast(delta_x), static_cast(delta_y) }); - if (target.blocked_by_main_thread_region || target.blocked_by_wheel_event_region || !target.node_id.has_value()) + { static_cast(delta_x), static_cast(delta_y) }, + Compositor::snap_container_handling_for(wheel_delta_precision_from(precise), scroll_gesture_phase_from(phase))); + if (target.blocked_by_main_thread_region) + return "main-thread"_utf16; + if (target.blocked_by_wheel_event_region || !target.node_id.has_value()) return "none"_utf16; if (scroll_tree.scroll_node_is_viewport(*target.node_id)) return "viewport"_utf16; diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index a894f97153aa5..dd390fdceece3 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -63,7 +64,7 @@ class WEB_API Internals final : public InternalsBase { GC::Ptr hit_test_result(double x, double y); void send_text(HTML::HTMLElement&, Utf16String const&, WebIDL::UnsignedShort modifiers); - void send_key(HTML::HTMLElement&, Utf16String const&, WebIDL::UnsignedShort modifiers); + void send_key(HTML::HTMLElement&, Utf16String const&, WebIDL::UnsignedShort modifiers, WebIDL::UnsignedLong repeat_count); void paste(HTML::HTMLElement& target, Utf16String const& text); void paste_from_clipboard(); void commit_text(); @@ -77,8 +78,8 @@ class WEB_API Internals final : public InternalsBase { // High-level mouse conveniences void click(double x, double y, WebIDL::UnsignedShort click_count, WebIDL::UnsignedShort button, WebIDL::UnsignedShort modifiers); void click_and_hold(double x, double y, WebIDL::UnsignedShort click_count, WebIDL::UnsignedShort button, WebIDL::UnsignedShort modifiers); - void wheel(GC::Ref, double x, double y, double delta_x, double delta_y); - void wheel(double x, double y, double delta_x, double delta_y, GC::Ref); + void wheel(GC::Ref, double x, double y, double delta_x, double delta_y, bool precise, Bindings::ScrollGesturePhase); + void wheel(double x, double y, double delta_x, double delta_y, bool precise, Bindings::ScrollGesturePhase, GC::Ref); void pinch(double x, double y, double scale_delta, WebIDL::UnsignedShort modifiers); void reset_zoom(); @@ -205,10 +206,10 @@ class WEB_API Internals final : public InternalsBase { Compositor::AsyncScrollingState async_scrolling_state(); GC::Ref async_scrolling_state_object(); bool async_scrolling_state_blocks_wheel_event_at(double x, double y); - bool async_scrolling_state_can_wheel_scroll_at(double x, double y, double delta_x, double delta_y, bool force_stale_wheel_event_regions); + bool async_scrolling_state_can_wheel_scroll_at(double x, double y, double delta_x, double delta_y, bool precise, bool force_stale_wheel_event_regions); Utf16String async_scrolling_state_wheel_routing_admission(); - Utf16String async_scrolling_state_wheel_scroll_admission_at(double x, double y, double delta_x, double delta_y, bool force_stale_wheel_event_regions); - Utf16String async_scrolling_state_wheel_target_at(double x, double y, double delta_x, double delta_y); + Utf16String async_scrolling_state_wheel_scroll_admission_at(double x, double y, double delta_x, double delta_y, bool precise, bool force_stale_wheel_event_regions); + Utf16String async_scrolling_state_wheel_target_at(double x, double y, double delta_x, double delta_y, bool precise, Bindings::ScrollGesturePhase); String viewport_overflow_x(); private: diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index eb58e7934af94..3016af2882747 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -32,7 +32,7 @@ interface Internals { const unsigned short BUTTON_RIGHT = 2; undefined sendText(HTMLElement target, Utf16DOMString text, optional unsigned short modifiers = 0); - undefined sendKey(HTMLElement target, Utf16DOMString keyName, optional unsigned short modifiers = 0); + undefined sendKey(HTMLElement target, Utf16DOMString keyName, optional unsigned short modifiers = 0, optional unsigned long repeatCount = 1); undefined paste(HTMLElement target, Utf16DOMString text); undefined pasteFromClipboard(); undefined commitText(); @@ -46,7 +46,7 @@ interface Internals { // High-level mouse conveniences undefined click(double x, double y, optional unsigned short clickCount = 1, optional unsigned short button = 0, optional unsigned short modifiers = 0); undefined clickAndHold(double x, double y, optional unsigned short clickCount = 1, optional unsigned short button = 0, optional unsigned short modifiers = 0); - [CreatesPromise] Promise wheel(double x, double y, double deltaX, double deltaY); + [CreatesPromise] Promise wheel(double x, double y, double deltaX, double deltaY, optional boolean precise = false, optional ScrollGesturePhase scrollGesturePhase = "none"); undefined pinch(double x, double y, double scaleDelta, optional unsigned short modifiers = 0); undefined resetZoom(); @@ -208,10 +208,12 @@ interface Internals { [ImplementedAs=async_scrolling_state_object] object asyncScrollingState(); boolean asyncScrollingStateBlocksWheelEventAt(double x, double y); - boolean asyncScrollingStateCanWheelScrollAt(double x, double y, double deltaX, double deltaY, optional boolean forceStaleWheelEventRegions = false); + boolean asyncScrollingStateCanWheelScrollAt(double x, double y, double deltaX, double deltaY, optional boolean precise = false, optional boolean forceStaleWheelEventRegions = false); Utf16DOMString asyncScrollingStateWheelRoutingAdmission(); - Utf16DOMString asyncScrollingStateWheelScrollAdmissionAt(double x, double y, double deltaX, double deltaY, optional boolean forceStaleWheelEventRegions = false); - Utf16DOMString asyncScrollingStateWheelTargetAt(double x, double y, double deltaX, double deltaY); + Utf16DOMString asyncScrollingStateWheelScrollAdmissionAt(double x, double y, double deltaX, double deltaY, optional boolean precise = false, optional boolean forceStaleWheelEventRegions = false); + Utf16DOMString asyncScrollingStateWheelTargetAt(double x, double y, double deltaX, double deltaY, optional boolean precise = false, optional ScrollGesturePhase scrollGesturePhase = "none"); DOMString viewportOverflowX(); }; + +enum ScrollGesturePhase { "none", "ongoing", "momentum", "ended" }; diff --git a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp index 8a2e0be5ece5b..b1f126ddfcb38 100644 --- a/Libraries/LibWeb/Layout/LayoutRustBridge.cpp +++ b/Libraries/LibWeb/Layout/LayoutRustBridge.cpp @@ -524,6 +524,8 @@ RustFFI::FfiCommitSink LayoutRustBridge::commit_sink() node.set_paintable(paintable); if (node.kind() == RustFFI::NodeKind::NavigableContainerViewport && paintable) bridge.m_committed_navigable_container_viewports.append(*paintable); + if (paintable && Painting::is_scroll_snap_container(*paintable)) + node.document().register_scroll_snap_container(*paintable); } else if (node.paintable_ptr()) { // A paintable surviving from a previous layout on a node this pass did not lay out is // stale; drop it so the layout tree only points into the paint tree built by this commit. diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 523c252532b57..f058c05aa7c55 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -1105,6 +1106,18 @@ void NodeWithStyle::bind_generated_style_record(CSS::StyleRecordID target_style_ publish_style_record_to_node_data(); } +static RefPtr scroll_snap_container_of(NodeWithStyle const& node) +{ + // The scroll snap properties specified on the root element apply to the viewport rather than to its own box. + if (node.is_viewport() || (node.dom_node() && node.dom_node() == node.document().document_element())) { + auto const* layout_viewport = node.document().unsafe_layout_node(); + return layout_viewport ? layout_viewport->paintable() : nullptr; + } + if (!node.is_scroll_container()) + return nullptr; + return node.paintable(); +} + void NodeWithStyle::publish_style_record_to_node_data() { auto const* payloads = document().style_computer().style_engine().style_record_payloads(m_style_record_identity); @@ -1112,6 +1125,26 @@ void NodeWithStyle::publish_style_record_to_node_data() node_data().style = payloads; if (content_visibility() == CSS::ContentVisibility::Auto) document().note_content_visibility_auto_style(); + + if (scroll_snap_type().strictness != CSS::ScrollSnapStrictness::None) + document().set_may_have_scroll_snap_areas(); + + // NB: A box whose layout node this style update is still building cannot be told apart from a snap container yet, + // and is looked at again by the update that completes it. + auto snap_container = scroll_snap_container_of(*this); + if (!snap_container || !snap_container->has_layout_node()) + return; + + // A style change can make a box a snap container without the paint tree being built again, so the box registers + // itself here as well as when it is built. + if (Painting::is_scroll_snap_container(*snap_container)) { + document().register_scroll_snap_container(*snap_container); + return; + } + + // A box that does not snap is snapped to no snap areas, so that a scroll it is given while it does not snap is not + // undone by a re-snap once it snaps again. + document().forget_snapped_areas_of_scroll_container(*snap_container); } bool NodeWithStyle::synchronize_table_span_data() diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index 8ef3af6e0b3af..0b516194b40df 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -598,7 +598,11 @@ class WEB_API NodeWithStyle : public Node { CSS::PointerEvents pointer_events() const { return style_group().pointer_events_value(); } CSS::ScrollbarColorData scrollbar_color() const { return style_group().scrollbar_color_value(); } CSS::Appearance appearance() const { return static_cast(style_group().appearance); } + CSS::LengthBox scroll_margin() const { return length_box(style_group().scroll_margin); } CSS::LengthBox scroll_padding() const { return length_box(style_group().scroll_padding); } + CSS::ScrollSnapAlignData scroll_snap_align() const { return style_group().scroll_snap_align_value(); } + CSS::ScrollSnapStop scroll_snap_stop() const { return static_cast(style_group().scroll_snap_stop); } + CSS::ScrollSnapType scroll_snap_type() const { return style_group().scroll_snap_type_value(); } CSS::ScrollbarWidth scrollbar_width() const { return static_cast(style_group().scrollbar_width); } CSS::UserSelect user_select() const { return static_cast(style_group().user_select); } CSS::WillChange will_change() const { return style_group().will_change_value(); } diff --git a/Libraries/LibWeb/Page/AutoScrollHandler.cpp b/Libraries/LibWeb/Page/AutoScrollHandler.cpp index a139280e3eba2..7c6e992d9afd5 100644 --- a/Libraries/LibWeb/Page/AutoScrollHandler.cpp +++ b/Libraries/LibWeb/Page/AutoScrollHandler.cpp @@ -76,6 +76,8 @@ AutoScrollHandler::AutoScrollHandler(HTML::LocalNavigable& navigable, DOM::Eleme { } +AutoScrollHandler::~AutoScrollHandler() = default; + void AutoScrollHandler::visit_edges(JS::Cell::Visitor& visitor) const { visitor.visit(m_navigable); @@ -144,6 +146,11 @@ RefPtr AutoScrollHandler::auto_scroll_paintable(DOM::Elemen void AutoScrollHandler::activate() { m_active = true; + + // Moving the mouse back inside the scrollport pauses the scrolling without ending the selection drag it belongs + // to, so the hold outlives deactivation and is released when the handler is torn down. + if (!m_scroll_gesture_hold) + m_scroll_gesture_hold = make(m_navigable); } void AutoScrollHandler::deactivate() @@ -196,6 +203,7 @@ void AutoScrollHandler::perform_tick() int scroll_y = m_fractional_delta.y().to_int(); m_fractional_delta -= CSSPixelPoint { scroll_x, scroll_y }; + m_navigable->note_user_scroll_input_intent(Painting::SnapSelectionStrategy::Type::EndPosition); if (paintable_box->scroll_by(scroll_x, scroll_y) == Painting::Paintable::ScrollHandled::No) return; diff --git a/Libraries/LibWeb/Page/AutoScrollHandler.h b/Libraries/LibWeb/Page/AutoScrollHandler.h index 4e706ad58d36c..33082bf437523 100644 --- a/Libraries/LibWeb/Page/AutoScrollHandler.h +++ b/Libraries/LibWeb/Page/AutoScrollHandler.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -16,6 +17,7 @@ namespace Web { class AutoScrollHandler { public: AutoScrollHandler(HTML::LocalNavigable&, DOM::Element& container); + ~AutoScrollHandler(); void visit_edges(JS::Cell::Visitor&) const; @@ -35,6 +37,7 @@ class AutoScrollHandler { GC::Ref m_container_element; CSSPixelPoint m_mouse_position; CSSPixelPoint m_fractional_delta; + OwnPtr m_scroll_gesture_hold; bool m_active { false }; }; diff --git a/Libraries/LibWeb/Page/EventHandler.cpp b/Libraries/LibWeb/Page/EventHandler.cpp index 183ed259c5e86..5ec2dadaa6fa8 100644 --- a/Libraries/LibWeb/Page/EventHandler.cpp +++ b/Libraries/LibWeb/Page/EventHandler.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -703,7 +704,57 @@ static CSSPixelPoint compute_mouse_event_offset(CSSPixelPoint position, Painting return offset; } -EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers, double wheel_delta_x, double wheel_delta_y, bool async_scroll_performed_default_action, Optional* async_scroll_operation) +struct VisualViewportPanAxes { + bool x { false }; + bool y { false }; + + bool is_empty() const { return !x && !y; } +}; + +static VisualViewportPanAxes visual_viewport_pan_axes_for_scroll_step(DOM::Document& document, double delta_x, double delta_y) +{ + auto visual_viewport = document.visual_viewport(); + auto maximum_offset_left = document.viewport_rect().width().to_double() - visual_viewport->width(); + auto maximum_offset_top = document.viewport_rect().height().to_double() - visual_viewport->height(); + return { + .x = (delta_x < 0 && visual_viewport->offset_left() > 0) || (delta_x > 0 && visual_viewport->offset_left() < maximum_offset_left), + .y = (delta_y < 0 && visual_viewport->offset_top() > 0) || (delta_y > 0 && visual_viewport->offset_top() < maximum_offset_top), + }; +} + +static RefPtr scrolling_box_for_scroll_step(Painting::Paintable& target, CSSPixelPoint delta) +{ + auto scrolling_box_moved_by = [](Painting::Paintable const& paintable, CSSPixelPoint delta) { + return paintable.clamp_scroll_offset(paintable.scroll_offset() + delta) != paintable.scroll_offset(); + }; + + auto deltas_the_scrolling_box_accepts = [](Painting::Paintable const& paintable, CSSPixelPoint delta) { + if (!paintable.could_be_scrolled_by_wheel_event(Painting::Paintable::ScrollDirection::Horizontal)) + delta.set_x(0); + if (!paintable.could_be_scrolled_by_wheel_event(Painting::Paintable::ScrollDirection::Vertical)) + delta.set_y(0); + return delta; + }; + + for (RefPtr paintable = target; paintable && !paintable->is_viewport_paintable(); paintable = paintable->containing_block()) { + auto accepted_delta = deltas_the_scrolling_box_accepts(*paintable, delta); + if (!accepted_delta.is_zero() && scrolling_box_moved_by(*paintable, accepted_delta)) + return paintable; + } + + // The viewport is scrolled by the default action itself rather than by the chain above, so it comes last. + auto viewport_paintable = target.document().paintable(); + if (!viewport_paintable) + return nullptr; + auto accepted_delta = deltas_the_scrolling_box_accepts(*viewport_paintable, delta); + if (accepted_delta.is_zero() || !scrolling_box_moved_by(*viewport_paintable, accepted_delta)) + return nullptr; + if (!visual_viewport_pan_axes_for_scroll_step(target.document(), delta.x().to_double(), delta.y().to_double()).is_empty()) + return nullptr; + return viewport_paintable; +} + +EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_position, CSSPixelPoint screen_position, u32 button, u32 buttons, u32 modifiers, double wheel_delta_x, double wheel_delta_y, WheelDeltaPrecision wheel_delta_precision, ScrollGesturePhase scroll_gesture_phase, bool async_scroll_performed_default_action, Optional* async_scroll_operation) { record_last_known_mouse_position(visual_viewport_position, screen_position, buttons, modifiers); @@ -716,8 +767,14 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi if (!document->is_fully_active()) return EventResult::Dropped; + m_navigable->adopt_pending_async_scroll_offsets(); + m_navigable->note_user_scroll_gesture_phase(scroll_gesture_phase); + // Wheel activity marks the scroll gesture as still in progress even when it no longer moves any scrolling box. m_navigable->defer_user_scroll_settlement(); + m_navigable->note_user_scroll_input_intent(wheel_delta_precision == WheelDeltaPrecision::Discrete + ? Painting::SnapSelectionStrategy::Type::Direction + : Painting::SnapSelectionStrategy::Type::EndPosition); auto visual_viewport = document->visual_viewport(); @@ -733,13 +790,39 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi if (auto result = target_for_mouse_position(visual_viewport_position); result.has_value()) paintable = result->paintable; + CSSPixelPoint wheel_step_delta { CSSPixels::nearest_value_for(wheel_delta_x), CSSPixels::nearest_value_for(wheel_delta_y) }; + + auto perform_snapped_wheel_scroll = [&](RefPtr target) { + bool is_discrete_step = wheel_delta_precision == WheelDeltaPrecision::Discrete; + if (!target || (!is_discrete_step && scroll_gesture_phase != ScrollGesturePhase::Momentum)) + return false; + auto scrolling_box = scrolling_box_for_scroll_step(*target, wheel_step_delta); + if (!scrolling_box) + return false; + if (is_discrete_step) + return m_navigable->perform_a_snapped_relative_user_scroll(*scrolling_box, wheel_step_delta, Painting::SnapSelectionStrategy::Type::Direction, HTML::LocalNavigable::SnapStepAccumulation::UntilGestureSettles); + return m_navigable->perform_a_snapped_momentum_scroll(*scrolling_box, wheel_step_delta); + }; + + auto wheel_step_selects_a_snap_position = [&] { + auto scrolling_box = scrolling_box_for_scroll_step(*paintable, wheel_step_delta); + if (!scrolling_box) + return false; + auto snap_axes = Painting::snap_axes_of_scroll_container(*scrolling_box); + return (snap_axes.x && wheel_step_delta.x() != 0) || (snap_axes.y && wheel_step_delta.y() != 0); + }; + auto can_attempt_async_scroll = m_navigable->page().async_scrolling_enabled() && m_navigable->has_compositor_context(); if (can_attempt_async_scroll && async_scroll_performed_default_action) { dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: default action already performed"); } else if (can_attempt_async_scroll && visual_viewport->scale() != 1.0) { dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: visual viewport is scaled"); } else if (can_attempt_async_scroll) { - if (paintable) { + if (paintable && wheel_delta_precision == WheelDeltaPrecision::Discrete && wheel_step_selects_a_snap_position()) { + // The step's snap position is selected by the default action below, so that the wheel event still has its + // chance to cancel the scroll and a nested navigable still gets the step first. + dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Not attempting wheel async scroll: the step selects a snap position"); + } else if (paintable) { auto viewport_rect = m_navigable->page().css_to_device_rect(m_navigable->viewport_rect()).to_type(); auto async_scroll_delta = Gfx::FloatPoint { static_cast(wheel_delta_x), static_cast(wheel_delta_y) }; auto device_position = m_navigable->page().css_to_device_point(visual_viewport_position); @@ -749,8 +832,9 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi auto operation_tracking = async_scroll_operation ? Compositor::AsyncScrollOperationTracking::Yes : Compositor::AsyncScrollOperationTracking::No; + auto snap_container_handling = Compositor::snap_container_handling_for(wheel_delta_precision, scroll_gesture_phase); auto enqueue_result = m_navigable->compositor_context().async_scroll_by( - document->unique_id(), async_scroll_position, async_scroll_delta_in_device_pixels, viewport_rect, operation_tracking); + document->unique_id(), async_scroll_position, async_scroll_delta_in_device_pixels, viewport_rect, snap_container_handling, operation_tracking); async_scroll_performed_default_action = enqueue_result.accepted; if (enqueue_result.operation_id.has_value() && async_scroll_operation) *async_scroll_operation = AsyncScrollOperation { m_navigable, *enqueue_result.operation_id }; @@ -781,6 +865,9 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi }; auto perform_wheel_default_action = [&](RefPtr target) -> EventResult { + if (perform_snapped_wheel_scroll(target)) + return EventResult::Handled; + RefPtr containing_block = move(target); while (containing_block) { auto handled_scroll_event = containing_block->handle_mousewheel({}, visual_viewport_position, buttons, modifiers, wheel_delta_x, wheel_delta_y); @@ -794,15 +881,9 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi if (!document || !document->paintable_box()) return EventResult::Dropped; - auto visual_viewport = document->visual_viewport(); - auto visual_viewport_max_x = m_navigable->viewport_rect().width().to_double() - visual_viewport->width(); - auto visual_viewport_max_y = m_navigable->viewport_rect().height().to_double() - visual_viewport->height(); - auto visual_viewport_can_scroll_horizontally = (wheel_delta_x < 0 && visual_viewport->offset_left() > 0) - || (wheel_delta_x > 0 && visual_viewport->offset_left() < visual_viewport_max_x); - auto visual_viewport_can_scroll_vertically = (wheel_delta_y < 0 && visual_viewport->offset_top() > 0) - || (wheel_delta_y > 0 && visual_viewport->offset_top() < visual_viewport_max_y); - auto viewport_wheel_delta_x = document->paintable_box()->could_be_scrolled_by_wheel_event(Painting::Paintable::ScrollDirection::Horizontal) || visual_viewport_can_scroll_horizontally ? wheel_delta_x : 0; - auto viewport_wheel_delta_y = document->paintable_box()->could_be_scrolled_by_wheel_event(Painting::Paintable::ScrollDirection::Vertical) || visual_viewport_can_scroll_vertically ? wheel_delta_y : 0; + auto visual_viewport_pan_axes = visual_viewport_pan_axes_for_scroll_step(*document, wheel_delta_x, wheel_delta_y); + auto viewport_wheel_delta_x = document->paintable_box()->could_be_scrolled_by_wheel_event(Painting::Paintable::ScrollDirection::Horizontal) || visual_viewport_pan_axes.x ? wheel_delta_x : 0; + auto viewport_wheel_delta_y = document->paintable_box()->could_be_scrolled_by_wheel_event(Painting::Paintable::ScrollDirection::Vertical) || visual_viewport_pan_axes.y ? wheel_delta_y : 0; if (viewport_wheel_delta_x != 0 || viewport_wheel_delta_y != 0) { auto viewport_scroll_position_before = CSSPixelPoint { CSSPixels(document->visual_viewport()->page_left()), CSSPixels(document->visual_viewport()->page_top()) }; @@ -815,8 +896,8 @@ EventResult EventHandler::handle_mousewheel(CSSPixelPoint visual_viewport_positi }; if (auto node = dom_node_for_event_dispatch(*paintable)) { - if (auto result = dispatch_event_to_nested_navigable(*paintable, visual_viewport_position, [screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, async_scroll_performed_default_action, async_scroll_operation](EventHandler& event_handler, CSSPixelPoint position) -> EventResult { - return event_handler.handle_mousewheel(position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, async_scroll_performed_default_action, async_scroll_operation); + if (auto result = dispatch_event_to_nested_navigable(*paintable, visual_viewport_position, [screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, wheel_delta_precision, scroll_gesture_phase, async_scroll_performed_default_action, async_scroll_operation](EventHandler& event_handler, CSSPixelPoint position) -> EventResult { + return event_handler.handle_mousewheel(position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, wheel_delta_precision, scroll_gesture_phase, async_scroll_performed_default_action, async_scroll_operation); }); result.has_value()) { if (result.value() == EventResult::Handled || result.value() == EventResult::Cancelled) @@ -1307,10 +1388,11 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u auto page_scroll_distance = document->window()->inner_height() - (document->window()->outer_height() - document->window()->inner_height()); // The held key keeps the scroll gesture in progress until it is released. - auto hold_scroll_gesture_until_key_release = [&] { + auto hold_scroll_gesture_until_key_release = [&](Painting::SnapSelectionStrategy::Type intent) { m_held_scroll_key = key; if (!m_scroll_key_gesture_hold) m_scroll_key_gesture_hold = make(*m_navigable); + m_navigable->note_user_scroll_input_intent(intent); }; auto scroll_target_for_key_input = [&]() -> GC::Ptr { if (auto focused_area = document->focused_area()) @@ -1336,20 +1418,36 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u } return false; }; - auto scroll_by_for_key_input = [&](CSSPixels delta_x, CSSPixels delta_y) { - hold_scroll_gesture_until_key_release(); + auto perform_snapped_scroll_step_for_key_input = [&](CSSPixelPoint delta, Painting::SnapSelectionStrategy::Type strategy_type) { + document->update_layout(DOM::UpdateLayoutReason::EventHandlerHandleKeyDown); + RefPtr target; + if (auto scroll_target = scroll_target_for_key_input()) + target = scroll_target->paintable(); + if (!target) + target = document->paintable(); + if (!target) + return false; + auto scrolling_box = scrolling_box_for_scroll_step(*target, delta); + if (!scrolling_box) + return false; + return m_navigable->perform_a_snapped_relative_user_scroll(*scrolling_box, delta, strategy_type, HTML::LocalNavigable::SnapStepAccumulation::UntilScrollFinishes); + }; + auto scroll_by_for_key_input = [&](CSSPixels delta_x, CSSPixels delta_y, Painting::SnapSelectionStrategy::Type intent) { + hold_scroll_gesture_until_key_release(intent); + if (perform_snapped_scroll_step_for_key_input({ delta_x, delta_y }, intent)) + return; if (scroll_container_of_scroll_target_by(delta_x.to_double(), delta_y.to_double())) return; m_navigable->scroll_viewport_by_delta({ delta_x, delta_y }, Bindings::ScrollBehavior::Auto); }; auto scroll_to_the_beginning_for_key_input = [&] { - hold_scroll_gesture_until_key_release(); + hold_scroll_gesture_until_key_release(Painting::SnapSelectionStrategy::Type::EndPosition); if (scroll_container_of_scroll_target_by(0, -CSSPixels::max().to_double())) return; m_navigable->perform_a_scroll_of_the_viewport({ 0, 0 }, Bindings::ScrollBehavior::Auto, HTML::LocalNavigable::ScrollTrigger::UserInput); }; auto scroll_to_the_end_for_key_input = [&] { - hold_scroll_gesture_until_key_release(); + hold_scroll_gesture_until_key_release(Painting::SnapSelectionStrategy::Type::EndPosition); if (scroll_container_of_scroll_target_by(0, CSSPixels::max().to_double())) return; m_navigable->scroll_viewport_by_delta({ 0, CSSPixels::max() }, Bindings::ScrollBehavior::Auto); @@ -1367,20 +1465,20 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u scroll_to_the_end_for_key_input(); } } else { - scroll_by_for_key_input(0, key == UIEvents::KeyCode::Key_Up ? -arrow_key_scroll_distance : arrow_key_scroll_distance); + scroll_by_for_key_input(0, key == UIEvents::KeyCode::Key_Up ? -arrow_key_scroll_distance : arrow_key_scroll_distance, Painting::SnapSelectionStrategy::Type::Direction); } return EventResult::Handled; case UIEvents::KeyCode::Key_Left: case UIEvents::KeyCode::Key_Right: if (modifiers_without_keypad) break; - scroll_by_for_key_input(key == UIEvents::KeyCode::Key_Left ? -arrow_key_scroll_distance : arrow_key_scroll_distance, 0); + scroll_by_for_key_input(key == UIEvents::KeyCode::Key_Left ? -arrow_key_scroll_distance : arrow_key_scroll_distance, 0, Painting::SnapSelectionStrategy::Type::Direction); return EventResult::Handled; case UIEvents::KeyCode::Key_PageUp: case UIEvents::KeyCode::Key_PageDown: if (modifiers_without_keypad != UIEvents::KeyModifier::Mod_None) break; - scroll_by_for_key_input(0, key == UIEvents::KeyCode::Key_PageUp ? -page_scroll_distance : page_scroll_distance); + scroll_by_for_key_input(0, key == UIEvents::KeyCode::Key_PageUp ? -page_scroll_distance : page_scroll_distance, Painting::SnapSelectionStrategy::Type::EndPositionAndDirection); return EventResult::Handled; case UIEvents::KeyCode::Key_Space: { if ((modifiers_without_keypad & ~UIEvents::KeyModifier::Mod_Shift) != UIEvents::KeyModifier::Mod_None) @@ -1394,7 +1492,7 @@ EventResult EventHandler::handle_keydown(UIEvents::KeyCode key, u32 modifiers, u if (focused_area_activates_on_space) break; bool scroll_backward = (modifiers_without_keypad & UIEvents::KeyModifier::Mod_Shift) != UIEvents::KeyModifier::Mod_None; - scroll_by_for_key_input(0, scroll_backward ? -page_scroll_distance : page_scroll_distance); + scroll_by_for_key_input(0, scroll_backward ? -page_scroll_distance : page_scroll_distance, Painting::SnapSelectionStrategy::Type::EndPositionAndDirection); return EventResult::Handled; } case UIEvents::KeyCode::Key_Home: @@ -1528,7 +1626,7 @@ EventResult EventHandler::handle_pinch_event(CSSPixelPoint point, u32 modifiers, auto offset_top_before_zoom = visual_viewport->offset_top(); visual_viewport->zoom(point, scale_delta); if (visual_viewport->offset_left() != offset_left_before_zoom || visual_viewport->offset_top() != offset_top_before_zoom) - m_navigable->queue_scrollend_event_after_user_scroll(*visual_viewport); + m_navigable->queue_scrollend_event_after_user_scroll(*visual_viewport, {}); return EventResult::Handled; } diff --git a/Libraries/LibWeb/Page/EventHandler.h b/Libraries/LibWeb/Page/EventHandler.h index 1f7589b82858a..6bca74fae2e2d 100644 --- a/Libraries/LibWeb/Page/EventHandler.h +++ b/Libraries/LibWeb/Page/EventHandler.h @@ -46,7 +46,7 @@ class WEB_API EventHandler { EventResult handle_mousedown(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, int click_count); EventResult handle_mousemove(CSSPixelPoint, CSSPixelPoint screen_position, unsigned buttons, unsigned modifiers); EventResult handle_mouseup(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers); - EventResult handle_mousewheel(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, double wheel_delta_x, double wheel_delta_y, bool async_scroll_performed_default_action = false, Optional* async_scroll_operation = nullptr); + EventResult handle_mousewheel(CSSPixelPoint, CSSPixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, double wheel_delta_x, double wheel_delta_y, WheelDeltaPrecision = WheelDeltaPrecision::Discrete, ScrollGesturePhase = ScrollGesturePhase::None, bool async_scroll_performed_default_action = false, Optional* async_scroll_operation = nullptr); EventResult handle_mouseleave(); #if defined(AK_OS_MACOS) bool select_word_for_dictionary_lookup(CSSPixelPoint visual_viewport_position); diff --git a/Libraries/LibWeb/Page/InputEvent.cpp b/Libraries/LibWeb/Page/InputEvent.cpp index 4a584d0cc5ecd..877b8b3f655e2 100644 --- a/Libraries/LibWeb/Page/InputEvent.cpp +++ b/Libraries/LibWeb/Page/InputEvent.cpp @@ -18,7 +18,7 @@ KeyEvent KeyEvent::clone_without_browser_data() const MouseEvent MouseEvent::clone_without_browser_data() const { - return { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, click_count, nullptr, async_scroll_performed_default_action }; + return { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, wheel_delta_precision, scroll_gesture_phase, click_count, nullptr, async_scroll_performed_default_action }; } DragEvent DragEvent::clone_without_browser_data() const @@ -64,6 +64,8 @@ ErrorOr IPC::encode(Encoder& encoder, Web::MouseEvent const& event) TRY(encoder.encode(event.modifiers)); TRY(encoder.encode(event.wheel_delta_x)); TRY(encoder.encode(event.wheel_delta_y)); + TRY(encoder.encode(event.wheel_delta_precision)); + TRY(encoder.encode(event.scroll_gesture_phase)); TRY(encoder.encode(event.click_count)); TRY(encoder.encode(event.async_scroll_performed_default_action)); return {}; @@ -80,10 +82,12 @@ ErrorOr IPC::decode(Decoder& decoder) auto modifiers = TRY(decoder.decode()); auto wheel_delta_x = TRY(decoder.decode()); auto wheel_delta_y = TRY(decoder.decode()); + auto wheel_delta_precision = TRY(decoder.decode()); + auto scroll_gesture_phase = TRY(decoder.decode()); auto click_count = TRY(decoder.decode()); auto async_scroll_performed_default_action = TRY(decoder.decode()); - return Web::MouseEvent { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, click_count, nullptr, async_scroll_performed_default_action }; + return Web::MouseEvent { type, position, screen_position, button, buttons, modifiers, wheel_delta_x, wheel_delta_y, wheel_delta_precision, scroll_gesture_phase, click_count, nullptr, async_scroll_performed_default_action }; } template<> diff --git a/Libraries/LibWeb/Page/InputEvent.h b/Libraries/LibWeb/Page/InputEvent.h index 28323d686ec18..fea857e29cd11 100644 --- a/Libraries/LibWeb/Page/InputEvent.h +++ b/Libraries/LibWeb/Page/InputEvent.h @@ -41,6 +41,22 @@ struct WEB_API KeyEvent { OwnPtr browser_data; }; +// Discrete wheel deltas come from stepwise input such as mouse wheel notches; precise wheel deltas come from input +// that reports exact pixel distances, such as touchpad panning gestures. +enum class WheelDeltaPrecision : u8 { + Discrete, + Precise, +}; + +// Input that scrolls with a gesture, such as a touchpad, reports whether the user is still making that gesture, +// whether a flick has handed the scrolling over to momentum, and when it ends. +enum class ScrollGesturePhase : u8 { + None, + Ongoing, + Momentum, + Ended, +}; + struct WEB_API MouseEvent { enum class Type : u8 { MouseDown, @@ -60,6 +76,8 @@ struct WEB_API MouseEvent { UIEvents::KeyModifier modifiers { UIEvents::KeyModifier::Mod_None }; double wheel_delta_x { 0 }; double wheel_delta_y { 0 }; + WheelDeltaPrecision wheel_delta_precision { WheelDeltaPrecision::Discrete }; + ScrollGesturePhase scroll_gesture_phase { ScrollGesturePhase::None }; int click_count { 0 }; OwnPtr browser_data; diff --git a/Libraries/LibWeb/Page/MiddleButtonScrollHandler.cpp b/Libraries/LibWeb/Page/MiddleButtonScrollHandler.cpp index 6e2029d84aad5..64c7eafc7d2a6 100644 --- a/Libraries/LibWeb/Page/MiddleButtonScrollHandler.cpp +++ b/Libraries/LibWeb/Page/MiddleButtonScrollHandler.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -87,6 +88,13 @@ void MiddleButtonScrollHandler::perform_tick() auto scroll_y = m_fractional_delta.y().to_int(); m_fractional_delta -= CSSPixelPoint { scroll_x, scroll_y }; + if (auto navigable = m_container_element->document().navigable()) { + // Middle button scrolling is one scroll gesture that runs until the mode it belongs to is exited, rather than + // until it runs out of scrolls, so it is held open for as long as this handler lives. + if (!m_scroll_gesture_hold) + m_scroll_gesture_hold = make(*navigable); + navigable->note_user_scroll_input_intent(Painting::SnapSelectionStrategy::Type::EndPosition); + } paintable_box->scroll_by(scroll_x, scroll_y); } diff --git a/Libraries/LibWeb/Page/MiddleButtonScrollHandler.h b/Libraries/LibWeb/Page/MiddleButtonScrollHandler.h index a1f446ea9bfd9..4d64c112aeef0 100644 --- a/Libraries/LibWeb/Page/MiddleButtonScrollHandler.h +++ b/Libraries/LibWeb/Page/MiddleButtonScrollHandler.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -33,6 +34,7 @@ class MiddleButtonScrollHandler { CSSPixelPoint m_origin; CSSPixelPoint m_mouse_position; CSSPixelPoint m_fractional_delta; + OwnPtr m_scroll_gesture_hold; bool m_mouse_has_moved_beyond_dead_zone { false }; }; diff --git a/Libraries/LibWeb/Page/Page.cpp b/Libraries/LibWeb/Page/Page.cpp index 87e60c2e97f6e..42537137bcf5a 100644 --- a/Libraries/LibWeb/Page/Page.cpp +++ b/Libraries/LibWeb/Page/Page.cpp @@ -387,9 +387,9 @@ UniqueNodeID Page::node_id_at_position(DevicePixelPoint position) return node->unique_id(); } -EventResult Page::handle_mousewheel(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, double wheel_delta_x, double wheel_delta_y, bool async_scroll_performed_default_action, Optional* async_scroll_operation) +EventResult Page::handle_mousewheel(DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, double wheel_delta_x, double wheel_delta_y, WheelDeltaPrecision wheel_delta_precision, ScrollGesturePhase scroll_gesture_phase, bool async_scroll_performed_default_action, Optional* async_scroll_operation) { - return top_level_traversable()->event_handler().handle_mousewheel(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, wheel_delta_x, wheel_delta_y, async_scroll_performed_default_action, async_scroll_operation); + return top_level_traversable()->event_handler().handle_mousewheel(device_to_css_point(position), device_to_css_point(screen_position), button, buttons, modifiers, wheel_delta_x, wheel_delta_y, wheel_delta_precision, scroll_gesture_phase, async_scroll_performed_default_action, async_scroll_operation); } EventResult Page::handle_drag_and_drop_event(DragEvent::Type type, DevicePixelPoint position, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector files) diff --git a/Libraries/LibWeb/Page/Page.h b/Libraries/LibWeb/Page/Page.h index bbca323ed1876..b8d1482b40152 100644 --- a/Libraries/LibWeb/Page/Page.h +++ b/Libraries/LibWeb/Page/Page.h @@ -148,7 +148,7 @@ class WEB_API Page final : public JS::Cell { bool select_word_for_dictionary_lookup(DevicePixelPoint); #endif UniqueNodeID node_id_at_position(DevicePixelPoint); - EventResult handle_mousewheel(DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, double wheel_delta_x, double wheel_delta_y, bool async_scroll_performed_default_action = false, Optional* async_scroll_operation = nullptr); + EventResult handle_mousewheel(DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, double wheel_delta_x, double wheel_delta_y, WheelDeltaPrecision = WheelDeltaPrecision::Discrete, ScrollGesturePhase = ScrollGesturePhase::None, bool async_scroll_performed_default_action = false, Optional* async_scroll_operation = nullptr); EventResult handle_drag_and_drop_event(DragEvent::Type, DevicePixelPoint, DevicePixelPoint screen_position, unsigned button, unsigned buttons, unsigned modifiers, Vector files); EventResult handle_pinch_event(DevicePixelPoint point, unsigned modifiers, double scale); diff --git a/Libraries/LibWeb/Painting/AccumulatedVisualContext.cpp b/Libraries/LibWeb/Painting/AccumulatedVisualContext.cpp index c27920df80e7c..d3e19ce889d30 100644 --- a/Libraries/LibWeb/Painting/AccumulatedVisualContext.cpp +++ b/Libraries/LibWeb/Painting/AccumulatedVisualContext.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -78,6 +79,20 @@ AccumulatedVisualContextTree AccumulatedVisualContextTree::create_with_content_o }); } +CSSPixelRect apply_css_transform_to_rect(Paintable const& paintable, CSSPixelRect const& rect) +{ + auto transform_data = rust_compute_css_transform(paintable, 1.0); + if (!transform_data.has_value()) + return rect; + + auto affine_transform = Gfx::extract_2d_affine_transform(transform_data->matrix); + auto transformed_rect = rect.to_type(); + transformed_rect.translate_by(-transform_data->origin); + transformed_rect = affine_transform.map(transformed_rect); + transformed_rect.translate_by(transform_data->origin); + return transformed_rect.to_type(); +} + VisualContextIndex AccumulatedVisualContextTree::append(VisualContextData data, VisualContextIndex parent_index) { VERIFY(parent_index.value() < m_nodes.size()); diff --git a/Libraries/LibWeb/Painting/AccumulatedVisualContext.h b/Libraries/LibWeb/Painting/AccumulatedVisualContext.h index d5e7226d34a73..ac1a479565ab1 100644 --- a/Libraries/LibWeb/Painting/AccumulatedVisualContext.h +++ b/Libraries/LibWeb/Painting/AccumulatedVisualContext.h @@ -128,6 +128,8 @@ struct AnchorScrollShift { using VisualContextData = Variant; +CSSPixelRect apply_css_transform_to_rect(Paintable const&, CSSPixelRect const&); + struct AccumulatedVisualContextNode { VisualContextData data; VisualContextIndex parent_index {}; diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index acbe46c809e61..cf47f1bb1a324 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -823,6 +823,47 @@ CSSPixelPoint Paintable::scroll_offset() const return {}; } +static Optional scroll_node_kind_for(Paintable const& paintable_box) +{ + if (paintable_box.is_viewport_paintable()) + return CompositorScrollNodeKind::Viewport; + if (paintable_box.layout_node().generated_for_pseudo_element().has_value()) + return CompositorScrollNodeKind::PseudoElement; + if (paintable_box.dom_node() && is(*paintable_box.dom_node())) + return CompositorScrollNodeKind::Element; + return {}; +} + +static UniqueNodeID scrollable_node_id_for(Paintable const& paintable_box) +{ + if (paintable_box.is_viewport_paintable()) + return paintable_box.document().unique_id(); + if (paintable_box.layout_node().generated_for_pseudo_element().has_value()) + return paintable_box.layout_node().pseudo_element_generator()->unique_id(); + return paintable_box.dom_node()->unique_id(); +} + +static u8 pseudo_element_type_for(Paintable const& paintable_box) +{ + auto pseudo_element = paintable_box.layout_node().generated_for_pseudo_element(); + if (!pseudo_element.has_value()) + return 0; + return static_cast(to_underlying(*pseudo_element)); +} + +Optional Paintable::async_scroll_node_stable_id() const +{ + auto scroll_node_kind = scroll_node_kind_for(*this); + if (!scroll_node_kind.has_value()) + return {}; + + return Compositor::AsyncScrollNodeStableID { + .node_id = scrollable_node_id_for(*this), + .kind = Compositor::async_scroll_node_kind_for(*scroll_node_kind), + .pseudo_element_type = pseudo_element_type_for(*this), + }; +} + CSSPixelPoint Paintable::minimum_scroll_offset() const { auto scrollable_overflow_rect = this->scrollable_overflow_rect(); @@ -943,14 +984,21 @@ Paintable::ScrollHandled Paintable::scroll_by(double delta_x, double delta_y) Paintable::ScrollHandled Paintable::set_scroll_offset_from_user_input(CSSPixelPoint offset) { - auto scroll_handled = set_scroll_offset(offset); auto navigable = document().navigable(); + auto stable_node_id = async_scroll_node_stable_id(); + + auto scroll_offset_before_scroll = scroll_offset(); + + if (navigable && stable_node_id.has_value()) + navigable->abort_in_flight_smooth_scrolls_taken_over_by_user_input(*stable_node_id, scroll_offset_before_scroll); + + auto scroll_handled = set_scroll_offset(offset); if (!navigable) return scroll_handled; if (scroll_handled == ScrollHandled::Yes) { if (auto event_target = scroll_event_target()) - navigable->queue_scrollend_event_after_user_scroll(*event_target); + navigable->queue_scrollend_event_after_user_scroll(*event_target, stable_node_id, scroll_offset_before_scroll); } else { // User input keeps the scroll gesture in progress even when it does not move the scrolling box. navigable->defer_user_scroll_settlement(); diff --git a/Libraries/LibWeb/Painting/Paintable.h b/Libraries/LibWeb/Painting/Paintable.h index 088648bdd00ba..2a763d7e603f2 100644 --- a/Libraries/LibWeb/Painting/Paintable.h +++ b/Libraries/LibWeb/Painting/Paintable.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -360,6 +361,8 @@ class WEB_API Paintable ScrollHandled scroll_by(double delta_x, double delta_y); void scroll_into_view(CSSPixelRect); + Optional async_scroll_node_stable_id() const; + CSSPixelSize content_size() const; CSSPixels content_width() const { return content_size().width(); } CSSPixels content_height() const { return content_size().height(); } diff --git a/Libraries/LibWeb/Painting/PaintingRustBridge.cpp b/Libraries/LibWeb/Painting/PaintingRustBridge.cpp index eeca85eb4a578..30be9e4d632cd 100644 --- a/Libraries/LibWeb/Painting/PaintingRustBridge.cpp +++ b/Libraries/LibWeb/Painting/PaintingRustBridge.cpp @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -492,6 +493,25 @@ bool rust_update_accumulated_visual_context_values(ViewportPaintable& viewport_p return Layout::RustFFI::layout_arena_update_visual_context_values(viewport_paintable.rust_arena().handle(), paintable_box.rust_slot(), visual_context_host_callbacks(viewport_paintable)); } +Optional rust_compute_css_transform(Paintable const& paintable_box, double pixel_ratio) +{ + auto viewport_paintable = const_cast(paintable_box.document()).unsafe_paintable(); + if (!viewport_paintable) + return {}; + float matrix_values[16]; + float origin_values[2]; + if (!Layout::RustFFI::layout_arena_compute_css_transform(viewport_paintable->rust_arena().handle(), paintable_box.rust_slot(), visual_context_host_callbacks(*viewport_paintable), pixel_ratio, matrix_values, origin_values)) + return {}; + return TransformData { + Gfx::FloatMatrix4x4( + matrix_values[0], matrix_values[1], matrix_values[2], matrix_values[3], + matrix_values[4], matrix_values[5], matrix_values[6], matrix_values[7], + matrix_values[8], matrix_values[9], matrix_values[10], matrix_values[11], + matrix_values[12], matrix_values[13], matrix_values[14], matrix_values[15]), + { origin_values[0], origin_values[1] }, + }; +} + Layout::RustFFI::FfiPhysicalOverflowDirections rust_physical_overflow_directions(Paintable const& paintable_box) { return Layout::RustFFI::layout_arena_physical_overflow_directions(paintable_box.rust_arena().handle(), paintable_box.rust_slot()); @@ -805,6 +825,11 @@ Layout::RustFFI::FfiPaintHostCallbacks paint_host_callbacks(PaintHostContext& co facts.scrollable_node_id = dom_node->unique_id().value(); } facts.pseudo_element_type = layout_node.generated_for_pseudo_element().has_value() ? static_cast(to_underlying(*layout_node.generated_for_pseudo_element())) : 0; + if (facts.scroll_node_kind != Layout::RustFFI::FfiScrollNodeKind::None) { + auto snap_axes = snap_axes_of_scroll_container(paintable); + facts.snaps_scroll_position_horizontally = snap_axes.x; + facts.snaps_scroll_position_vertically = snap_axes.y; + } facts.inside_blocking_wheel_event_handler = dom_node && dom_node->inside_blocking_wheel_event_handler(); facts.records_viewport_scrollbars = paintable.is_viewport_paintable() && paintable.document().page().async_scrolling_enabled() diff --git a/Libraries/LibWeb/Painting/PaintingRustBridge.h b/Libraries/LibWeb/Painting/PaintingRustBridge.h index f2df918d277a8..50fac647de612 100644 --- a/Libraries/LibWeb/Painting/PaintingRustBridge.h +++ b/Libraries/LibWeb/Painting/PaintingRustBridge.h @@ -38,6 +38,7 @@ WEB_API bool rust_assign_accumulated_visual_contexts(ViewportPaintable&, bool fo WEB_API AccumulatedVisualContextTree materialize_rust_main_visual_context_tree(ViewportPaintable&); WEB_API void patch_rust_visual_context_nodes(ViewportPaintable&, AccumulatedVisualContextTree&, size_t begin, size_t end); WEB_API bool rust_update_accumulated_visual_context_values(ViewportPaintable&, Paintable&); +WEB_API Optional rust_compute_css_transform(Paintable const&, double pixel_ratio); WEB_API Layout::RustFFI::FfiPhysicalOverflowDirections rust_physical_overflow_directions(Paintable const&); WEB_API void rust_measure_scrollable_overflow(Paintable const&); WEB_API CSS::ResolvedImage rust_resolve_gradient_for_size(CSS::StyleValue const&, Layout::NodeWithStyle const&, CSSPixelSize); diff --git a/Libraries/LibWeb/Painting/ScrollSnap.cpp b/Libraries/LibWeb/Painting/ScrollSnap.cpp new file mode 100644 index 0000000000000..35ccc92d6b4be --- /dev/null +++ b/Libraries/LibWeb/Painting/ScrollSnap.cpp @@ -0,0 +1,936 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Painting { + +// A closed range of scroll offsets that are all valid snap positions. +struct CoveringRange { + CSSPixels start; + CSSPixels end; +}; + +// A candidate scroll offset in one axis, produced by aligning a snap area with the snapport. +struct SnapPositionCandidate { + CSSPixels offset; + SnapAreaReference area {}; + + Vector covering_ranges; + + // The open range of scroll offsets in the container's other axis at which the snap area overlaps the snapport. + CSSPixels cross_axis_visible_range_start { 0 }; + CSSPixels cross_axis_visible_range_end { 0 }; + + bool always_stop { false }; +}; + +struct SnapAxisCandidates { + Vector x_candidates; + Vector y_candidates; +}; + +static SnapAreaReference snap_area_reference_for(Paintable const& snap_area) +{ + auto const& layout_node = snap_area.layout_node(); + if (auto pseudo_element = layout_node.generated_for_pseudo_element(); pseudo_element.has_value()) + return { layout_node.pseudo_element_generator().ptr(), pseudo_element }; + return { as_if(snap_area.dom_node().ptr()), {} }; +} + +static Layout::NodeWithStyle const* style_source_for_snap_container(Paintable const& snap_container) +{ + if (snap_container.layout_node().is_viewport()) { + auto const* document_element = snap_container.document().document_element(); + if (!document_element) + return nullptr; + return document_element->unsafe_layout_node(); + } + return &snap_container.layout_node(); +} + +static bool has_snap_alignment(CSS::ScrollSnapAlignData alignment) +{ + return alignment.block_alignment != CSS::ScrollSnapAlign::None || alignment.inline_alignment != CSS::ScrollSnapAlign::None; +} + +static bool is_captured_by_snap_container(Paintable const& snap_area, Paintable const& snap_container) +{ + for (auto containing_block = snap_area.containing_block(); containing_block; containing_block = containing_block->containing_block()) { + if (containing_block.ptr() == &snap_container) + return true; + // The box whose overflow was propagated to the viewport is left with a used overflow of visible, so it is not + // a scroll container and cannot capture snap areas of its own. + if (containing_block->layout_node().is_scroll_container()) + return false; + } + return false; +} + +template +static void for_each_descendant_snap_area(Paintable const& parent, Paintable const& snap_container, Callback const& callback) +{ + parent.for_each_child([&](Paintable const& child) { + // Snap areas are captured by the nearest scroll container in their containing block chain, so areas inside a + // nested scroll container may still belong to an outer container when they are positioned. + if (has_snap_alignment(child.layout_node().scroll_snap_align()) && is_captured_by_snap_container(child, snap_container)) + callback(child); + for_each_descendant_snap_area(child, snap_container, callback); + return IterationDecision::Continue; + }); +} + +// https://drafts.csswg.org/css-scroll-snap-1/#scroll-margin +static CSSPixelRect snap_area_rect(Paintable const& snap_area, Paintable const& snap_container) +{ + // The scroll snap area is determined by taking the transformed border box, finding its rectangular bounding box + // (axis-aligned in the scroll container's coordinate space), then adding the specified outsets. + + // NB: A snap area is captured by the nearest scroll container in its containing block chain, so the boxes between + // an area and its container contribute transforms only, and mapping the border box through each of them in + // turn lands it in the container's coordinate space. + auto rect = apply_css_transform_to_rect(snap_area, snap_area.absolute_border_box_rect()); + for (auto containing_block = snap_area.containing_block(); containing_block && containing_block.ptr() != &snap_container; containing_block = containing_block->containing_block()) + rect = apply_css_transform_to_rect(*containing_block, rect); + + auto const& scroll_margin = snap_area.layout_node().scroll_margin(); + rect.inflate( + scroll_margin.top().to_px_or_zero(CSSPixels { 0 }), + scroll_margin.right().to_px_or_zero(CSSPixels { 0 }), + scroll_margin.bottom().to_px_or_zero(CSSPixels { 0 }), + scroll_margin.left().to_px_or_zero(CSSPixels { 0 })); + return rect; +} + +struct SnapAxisGeometry { + CSSPixels snapport_start; + CSSPixels snapport_size; + CSSPixels min_offset; + CSSPixels max_offset; +}; + +struct PhysicalSnapAlignment { + CSS::ScrollSnapAlign x; + CSS::ScrollSnapAlign y; +}; + +// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align +static PhysicalSnapAlignment physical_snap_alignment(CSS::ScrollSnapAlignData alignment, Layout::NodeWithStyle const& writing_mode_source) +{ + // The two values specify the snapping alignment in the block axis and inline axis, respectively, as determined by the + // snap container's writing mode. + + // NB: start and end name the edges an axis begins and ends at, which are its lesser and greater physical edges + // only while the axis runs in the same direction as the physical one. + auto alignment_along_axis = [](CSS::ScrollSnapAlign axis_alignment, bool axis_is_reverse) { + if (!axis_is_reverse) + return axis_alignment; + switch (axis_alignment) { + case CSS::ScrollSnapAlign::Start: + return CSS::ScrollSnapAlign::End; + case CSS::ScrollSnapAlign::End: + return CSS::ScrollSnapAlign::Start; + case CSS::ScrollSnapAlign::None: + case CSS::ScrollSnapAlign::Center: + return axis_alignment; + } + VERIFY_NOT_REACHED(); + }; + + bool horizontal_writing_mode = writing_mode_source.writing_mode() == CSS::WritingMode::HorizontalTb; + auto x_alignment = horizontal_writing_mode ? alignment.inline_alignment : alignment.block_alignment; + auto y_alignment = horizontal_writing_mode ? alignment.block_alignment : alignment.inline_alignment; + bool x_axis_is_reverse = horizontal_writing_mode ? writing_mode_source.inline_axis_is_reverse() : writing_mode_source.block_axis_is_reverse(); + bool y_axis_is_reverse = horizontal_writing_mode ? writing_mode_source.block_axis_is_reverse() : writing_mode_source.inline_axis_is_reverse(); + return { + .x = alignment_along_axis(x_alignment, x_axis_is_reverse), + .y = alignment_along_axis(y_alignment, y_axis_is_reverse), + }; +} + +static Optional snap_position_candidate_for_axis(CSS::ScrollSnapAlign alignment, CSSPixels area_start, CSSPixels area_size, SnapAxisGeometry const& geometry) +{ + CSSPixels offset; + switch (alignment) { + case CSS::ScrollSnapAlign::None: + return {}; + case CSS::ScrollSnapAlign::Start: + offset = area_start - geometry.snapport_start; + break; + case CSS::ScrollSnapAlign::End: + offset = area_start + area_size - (geometry.snapport_start + geometry.snapport_size); + break; + case CSS::ScrollSnapAlign::Center: + offset = area_start + area_size / 2 - (geometry.snapport_start + geometry.snapport_size / 2); + break; + } + + // https://drafts.csswg.org/css-scroll-snap-1/#unreachable + // If a snap position is unreachable as specified, such that aligning to it would require scrolling the scroll + // container's viewport past the edge of its scrollable overflow area, the used snap position for this snap area is + // the position resulting from scrolling as much as possible in each relevant axis toward the desired snap position. + SnapPositionCandidate candidate { .offset = clamp(offset, geometry.min_offset, geometry.max_offset), .covering_ranges = {} }; + + if (area_size > geometry.snapport_size) { + candidate.covering_ranges.append({ + .start = clamp(area_start - geometry.snapport_start, geometry.min_offset, geometry.max_offset), + .end = clamp(area_start + area_size - (geometry.snapport_start + geometry.snapport_size), geometry.min_offset, geometry.max_offset), + }); + } + + return candidate; +} + +static void restrict_covering_ranges_to_valid_snap_positions(Vector& candidates, CSSPixels snapport_size) +{ + Vector snap_positions; + snap_positions.ensure_capacity(candidates.size()); + for (auto const& candidate : candidates) + snap_positions.unchecked_append(candidate.offset); + quick_sort(snap_positions); + + for (auto& candidate : candidates) { + if (candidate.covering_ranges.is_empty()) + continue; + + auto covering_range = candidate.covering_ranges.first(); + candidate.covering_ranges.clear_with_capacity(); + + auto append_valid_offsets_between = [&](CSSPixels start, CSSPixels end) { + start = max(start, covering_range.start); + end = min(end, covering_range.end); + if (start <= end) + candidate.covering_ranges.append({ .start = start, .end = end }); + }; + + append_valid_offsets_between(covering_range.start, snap_positions.first()); + for (size_t i = 1; i < snap_positions.size(); ++i) { + if (snap_positions[i] - snap_positions[i - 1] > snapport_size) + append_valid_offsets_between(snap_positions[i - 1], snap_positions[i]); + } + append_valid_offsets_between(snap_positions.last(), covering_range.end); + } +} + +// AD-HOC: Offsets within one pixel of the offset a scroll travels from count as being at it, so that a fractional +// scroll offset cannot re-select the snap position the scroll started from. This matches other engines. +static constexpr CSSPixels SNAP_POSITION_BOUNDARY_TOLERANCE = 1; + +static bool is_beyond_in_direction(CSSPixels offset, CSSPixels boundary, CSSPixels direction) +{ + if (direction > 0) + return offset >= boundary + SNAP_POSITION_BOUNDARY_TOLERANCE; + return offset <= boundary - SNAP_POSITION_BOUNDARY_TOLERANCE; +} + +static bool is_at_or_beyond_in_direction(CSSPixels offset, CSSPixels boundary, CSSPixels direction) +{ + if (direction > 0) + return offset >= boundary; + return offset <= boundary; +} + +struct SnapAxisSelection { + CSSPixels destination; + CSSPixels start; + CSSPixels direction; + Optional starting_positions_boundary; +}; + +static bool snap_area_is_visible_at_cross_axis_offset(SnapPositionCandidate const& candidate, CSSPixels cross_axis_offset) +{ + return cross_axis_offset > candidate.cross_axis_visible_range_start && cross_axis_offset < candidate.cross_axis_visible_range_end; +} + +// Every offset within a covering range is a valid snap position of the area that contributes it. +static bool candidate_has_snap_position_at(SnapPositionCandidate const& candidate, CSSPixels offset) +{ + if (candidate.offset == offset) + return true; + return any_of(candidate.covering_ranges, [&](auto const& covering_range) { return offset >= covering_range.start && offset <= covering_range.end; }); +} + +static bool chosen_offset_is_visible_at_cross_axis_offset(Vector const& candidates, CSSPixels offset, CSSPixels cross_axis_offset) +{ + return any_of(candidates, [&](auto const& candidate) { + return snap_area_is_visible_at_cross_axis_offset(candidate, cross_axis_offset) && candidate_has_snap_position_at(candidate, offset); + }); +} + +static SnapDestination snap_destination_for(CSSPixelPoint unsnapped_destination, Optional x_offset, Optional y_offset, SnapAxes evaluated_axes) +{ + return { + .position = { x_offset.value_or(unsnapped_destination.x()), y_offset.value_or(unsnapped_destination.y()) }, + .snapped_x = x_offset.has_value(), + .snapped_y = y_offset.has_value(), + .evaluated_x = evaluated_axes.x, + .evaluated_y = evaluated_axes.y, + }; +} + +static bool chosen_offsets_are_mutually_visible(SnapAxisCandidates const& candidates, CSSPixels x_offset, CSSPixels y_offset) +{ + return chosen_offset_is_visible_at_cross_axis_offset(candidates.x_candidates, x_offset, y_offset) + && chosen_offset_is_visible_at_cross_axis_offset(candidates.y_candidates, y_offset, x_offset); +} + +struct SnapAxisChoice { + CSSPixels offset; + SnapAreaReference area; +}; + +static Optional choose_snap_offset_for_axis(Vector const& candidates, SnapAxisSelection const& selection, CSSPixels snapport_size, CSS::ScrollSnapStrictness strictness, Optional cross_axis_offset, Optional only_area = {}) +{ + // AD-HOC: The parameters under which a proximity snap container snaps are left to the user agent. Match the + // threshold used by other engines, one third of the snapport size in the snapping axis. + auto proximity_range = snapport_size / 3; + + Optional best_choice; + CSSPixels best_distance = 0; + auto consider_candidate = [&](CSSPixels offset, SnapPositionCandidate const& candidate) { + auto distance = abs(offset - selection.destination); + if (strictness == CSS::ScrollSnapStrictness::Proximity && distance > proximity_range) + return; + if (!best_choice.has_value() || distance < best_distance) { + best_choice = SnapAxisChoice { offset, candidate.area }; + best_distance = distance; + } + }; + + Optional first_always_stop_choice; + auto track_always_stop_candidate = [&](SnapPositionCandidate const& candidate) { + if (!first_always_stop_choice.has_value() || abs(candidate.offset - selection.start) < abs(first_always_stop_choice->offset - selection.start)) + first_always_stop_choice = SnapAxisChoice { candidate.offset, candidate.area }; + }; + + for (auto const& candidate : candidates) { + if (only_area.has_value() && candidate.area != *only_area) + continue; + + // https://drafts.csswg.org/css-scroll-snap-1/#snap-scope + // Since the purpose of scroll snapping is to align content within the scrollport for optimal viewing, a + // scroll position cannot be considered a valid snap position if snapping to it would leave the contributing + // snap area entirely outside the snapport, even if it otherwise satisfies the required alignment of the snap + // area. + if (cross_axis_offset.has_value() && !snap_area_is_visible_at_cross_axis_offset(candidate, *cross_axis_offset)) + continue; + + if (selection.direction != 0 && candidate.always_stop && is_beyond_in_direction(candidate.offset, selection.start, selection.direction)) + track_always_stop_candidate(candidate); + + if (!selection.starting_positions_boundary.has_value() + || (is_beyond_in_direction(candidate.offset, selection.start, selection.direction) + && is_at_or_beyond_in_direction(candidate.offset, *selection.starting_positions_boundary, selection.direction))) + consider_candidate(candidate.offset, candidate); + + // NB: Every offset in a covering range is a valid snap position, so such a range contributes the offset in it + // nearest the destination. A relative scroll may not select the part of a range it has already traveled + // past, which is the part at or behind the offset it started from. + // FIXME: Limit covering ranges to the offsets at which no snap area with scroll-snap-stop: always has entered + // the snapport yet, so a scroll within a covering snap area still stops ahead of such an area. + for (auto const& covering_range : candidate.covering_ranges) { + auto range_start = covering_range.start; + auto range_end = covering_range.end; + if (selection.starting_positions_boundary.has_value()) { + if (selection.direction > 0) { + range_start = max(range_start, selection.start + SNAP_POSITION_BOUNDARY_TOLERANCE); + } else if (selection.direction < 0) { + range_end = min(range_end, selection.start - SNAP_POSITION_BOUNDARY_TOLERANCE); + } + } + if (range_start > range_end) + continue; + + consider_candidate(clamp(selection.destination, range_start, range_end), candidate); + } + } + + // https://drafts.csswg.org/css-scroll-snap-1/#valdef-scroll-snap-type-mandatory + // If a valid snap position exists then the scroll container must snap at the termination of a scroll (if none + // exist then no snapping occurs). + // NB: A mandatory container whose scroll has no snap position ahead of it in the direction of travel therefore + // falls back to the snap position nearest the destination. + if (!best_choice.has_value() && selection.starting_positions_boundary.has_value() && strictness == CSS::ScrollSnapStrictness::Mandatory) { + SnapAxisSelection fallback_selection { + .destination = selection.destination, + .start = selection.destination, + .direction = 0, + .starting_positions_boundary = {}, + }; + best_choice = choose_snap_offset_for_axis(candidates, fallback_selection, snapport_size, strictness, cross_axis_offset, only_area); + } + + // https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-stop + // always + // The scroll container must not pass over a snap position defined by this element during the execution of a + // scrolling operation; it must instead snap to the first of this element's snap positions. + if (best_choice.has_value() && first_always_stop_choice.has_value()) { + bool always_stop_position_is_passed_over = selection.direction > 0 + ? best_choice->offset > first_always_stop_choice->offset + : best_choice->offset < first_always_stop_choice->offset; + if (always_stop_position_is_passed_over) + best_choice = first_always_stop_choice; + } + + return best_choice; +} + +// https://drafts.csswg.org/css-scroll-snap-1/#snap-axis +SnapAxes snap_axes_of_scroll_container(Paintable const& snap_container) +{ + auto const* style_source = style_source_for_snap_container(snap_container); + if (!style_source) + return {}; + + auto snap_type = style_source->scroll_snap_type(); + if (snap_type.strictness == CSS::ScrollSnapStrictness::None) + return {}; + + bool horizontal_writing_mode = style_source->writing_mode() == CSS::WritingMode::HorizontalTb; + switch (snap_type.axis) { + case CSS::ScrollSnapAxis::X: + return { .x = true, .y = false }; + case CSS::ScrollSnapAxis::Y: + return { .x = false, .y = true }; + case CSS::ScrollSnapAxis::Inline: + return { .x = horizontal_writing_mode, .y = !horizontal_writing_mode }; + case CSS::ScrollSnapAxis::Block: + return { .x = !horizontal_writing_mode, .y = horizontal_writing_mode }; + case CSS::ScrollSnapAxis::Both: + return { .x = true, .y = true }; + } + VERIFY_NOT_REACHED(); +} + +struct SnapCandidateCollection { + CSSPixelRect snapport; + SnapAxisGeometry x_geometry; + SnapAxisGeometry y_geometry; + CSS::ScrollSnapType snap_type; + SnapAxisCandidates candidates; +}; + +static Optional collect_snap_position_candidates(Paintable const& snap_container, bool collect_x, bool collect_y) +{ + auto const* style_source = style_source_for_snap_container(snap_container); + if (!style_source) + return {}; + + auto snap_type = style_source->scroll_snap_type(); + + auto scrollable_overflow_rect = snap_container.scrollable_overflow_rect(); + if (!scrollable_overflow_rect.has_value()) + return {}; + + // https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding + // For a scroll snap container this region also defines the scroll snapport—the area of the scrollport that is + // used as the alignment container for the scroll snap areas when calculating snap positions. + auto snapport = snap_container.scroll_snapport_rect(); + + auto min_scroll_offset = snap_container.minimum_scroll_offset(); + auto max_scroll_offset = snap_container.maximum_scroll_offset(); + SnapCandidateCollection collection { + .snapport = snapport, + .x_geometry = { + .snapport_start = snapport.left(), + .snapport_size = snapport.width(), + .min_offset = min_scroll_offset.x(), + .max_offset = max_scroll_offset.x(), + }, + .y_geometry = { + .snapport_start = snapport.top(), + .snapport_size = snapport.height(), + .min_offset = min_scroll_offset.y(), + .max_offset = max_scroll_offset.y(), + }, + .snap_type = snap_type, + .candidates = {}, + }; + + for_each_descendant_snap_area(snap_container, snap_container, [&](Paintable const& snap_area) { + auto const& area_layout_node = snap_area.layout_node(); + auto always_stop = area_layout_node.scroll_snap_stop() == CSS::ScrollSnapStop::Always; + auto area_rect = snap_area_rect(snap_area, snap_container); + auto area_reference = snap_area_reference_for(snap_area); + + // https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align + // Start and end alignments are resolved with respect to the writing mode of the snap container unless the + // scroll snap area is larger than the snapport, in which case they are resolved with respect to the writing + // mode of the box itself. + // NB: The size the area is compared in is the one it lays its content out along, so that an area whose + // content no longer fits the snapport aligns the edge that content begins at. This matches other engines. + bool area_is_larger_than_snapport = area_layout_node.writing_mode() == CSS::WritingMode::HorizontalTb + ? area_rect.width() > snapport.width() + : area_rect.height() > snapport.height(); + auto alignment = physical_snap_alignment(area_layout_node.scroll_snap_align(), area_is_larger_than_snapport ? area_layout_node : *style_source); + + if (collect_x) { + if (auto candidate = snap_position_candidate_for_axis(alignment.x, area_rect.left(), area_rect.width(), collection.x_geometry); candidate.has_value()) { + candidate->area = area_reference; + candidate->always_stop = always_stop; + candidate->cross_axis_visible_range_start = area_rect.top() - collection.y_geometry.snapport_start - collection.y_geometry.snapport_size; + candidate->cross_axis_visible_range_end = area_rect.bottom() - collection.y_geometry.snapport_start; + collection.candidates.x_candidates.append(*candidate); + } + } + if (collect_y) { + if (auto candidate = snap_position_candidate_for_axis(alignment.y, area_rect.top(), area_rect.height(), collection.y_geometry); candidate.has_value()) { + candidate->area = area_reference; + candidate->always_stop = always_stop; + candidate->cross_axis_visible_range_start = area_rect.left() - collection.x_geometry.snapport_start - collection.x_geometry.snapport_size; + candidate->cross_axis_visible_range_end = area_rect.right() - collection.x_geometry.snapport_start; + collection.candidates.y_candidates.append(*candidate); + } + } + }); + + if (collect_x) + restrict_covering_ranges_to_valid_snap_positions(collection.candidates.x_candidates, collection.x_geometry.snapport_size); + if (collect_y) + restrict_covering_ranges_to_valid_snap_positions(collection.candidates.y_candidates, collection.y_geometry.snapport_size); + + return collection; +} + +static Vector snap_areas_at_offset(Vector const& candidates, CSSPixels offset, CSSPixels cross_axis_offset) +{ + Vector areas; + for (auto const& candidate : candidates) { + if (!snap_area_is_visible_at_cross_axis_offset(candidate, cross_axis_offset)) + continue; + if (candidate_has_snap_position_at(candidate, offset) && candidate.area.element) + areas.append(candidate.area); + } + return areas; +} + +// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-container +bool is_scroll_snap_container(Paintable const& paintable) +{ + if (!paintable.layout_node().is_scroll_container()) + return false; + return !snap_axes_of_scroll_container(paintable).is_empty(); +} + +// https://drafts.csswg.org/css-scroll-snap-1/#choosing +SnapDestination adjust_scroll_destination_for_snapping(Paintable const& snap_container, CSSPixelPoint destination, SnapSelectionStrategy const& strategy) +{ + auto snap_axes = snap_axes_of_scroll_container(snap_container); + if (snap_axes.is_empty()) + return { destination }; + + // NB: A scroll selects a snap position only in the axes it traveled in, so that the offset of an axis its input + // never moved is left where it is. A scroll that traveled in no axis, such as one with only an intended end + // position, selects a snap position in every axis the container snaps in. + auto snaps_in_axis = [&](bool container_snaps_in_axis, CSSPixels axis_displacement) { + return container_snaps_in_axis && (strategy.displacement.is_zero() || axis_displacement != 0); + }; + bool snaps_x = snaps_in_axis(snap_axes.x, strategy.displacement.x()); + bool snaps_y = snaps_in_axis(snap_axes.y, strategy.displacement.y()); + if (!snaps_x && !snaps_y) + return { destination }; + + auto collection = collect_snap_position_candidates(snap_container, snaps_x, snaps_y); + if (!collection.has_value()) + return { destination }; + + auto snap_type = collection->snap_type; + auto const& snapport = collection->snapport; + auto const& candidates = collection->candidates; + + auto axis_selection = [&](CSSPixels axis_destination, CSSPixels axis_displacement, Optional axis_start, Optional axis_boundary) { + if (!axis_start.has_value() || axis_displacement == 0) { + return SnapAxisSelection { + .destination = axis_destination, + .start = axis_destination, + .direction = 0, + .starting_positions_boundary = {}, + }; + } + + Optional boundary; + if (strategy.type != SnapSelectionStrategy::Type::EndPosition) + boundary = axis_boundary.value_or(*axis_start); + + return SnapAxisSelection { + .destination = axis_destination, + .start = *axis_start, + .direction = axis_displacement, + .starting_positions_boundary = boundary, + }; + }; + auto axis_of = [](Optional const& offset, bool horizontal) -> Optional { + if (!offset.has_value()) + return {}; + return horizontal ? offset->x() : offset->y(); + }; + auto x_selection = axis_selection(destination.x(), strategy.displacement.x(), axis_of(strategy.start_offset, true), axis_of(strategy.starting_positions_boundary, true)); + auto y_selection = axis_selection(destination.y(), strategy.displacement.y(), axis_of(strategy.start_offset, false), axis_of(strategy.starting_positions_boundary, false)); + + auto choose_x = [&](Optional cross_axis_offset, Optional only_area = {}) { + return choose_snap_offset_for_axis(candidates.x_candidates, x_selection, snapport.width(), snap_type.strictness, cross_axis_offset, only_area); + }; + auto choose_y = [&](Optional cross_axis_offset, Optional only_area = {}) { + return choose_snap_offset_for_axis(candidates.y_candidates, y_selection, snapport.height(), snap_type.strictness, cross_axis_offset, only_area); + }; + + Optional x_choice; + Optional y_choice; + if (snaps_x && snaps_y) { + x_choice = choose_x({}); + y_choice = choose_y({}); + if (x_choice.has_value() && y_choice.has_value() && !chosen_offsets_are_mutually_visible(candidates, x_choice->offset, y_choice->offset)) { + // AD-HOC: A snap area is visible at its own snap positions, so one axis keeps the position it chose while + // the other gives up its own and takes the one the same area offers. Of the two areas, the one + // leaving the scroll container nearest its destination is followed. This matches other engines. + Optional y_of_x_area; + Optional x_of_y_area; + if (x_choice->area.element) + y_of_x_area = choose_y(x_choice->offset, x_choice->area); + if (y_choice->area.element) + x_of_y_area = choose_x(y_choice->offset, y_choice->area); + + auto distance_to_destination = [&](SnapAxisChoice const& x_axis_choice, SnapAxisChoice const& y_axis_choice) { + return AK::hypot((x_axis_choice.offset - x_selection.destination).to_double(), (y_axis_choice.offset - y_selection.destination).to_double()); + }; + bool follows_x_area = y_of_x_area.has_value(); + if (follows_x_area && x_of_y_area.has_value()) + follows_x_area = distance_to_destination(*x_choice, *y_of_x_area) < distance_to_destination(*x_of_y_area, *y_choice); + + if (follows_x_area) { + y_choice = y_of_x_area; + } else if (x_of_y_area.has_value()) { + x_choice = x_of_y_area; + } else { + // NB: Neither area offers a snap position in both axes, so the axis whose chosen offset is farther + // from its destination is chosen again from the positions visible at the other axis's offset. + if (abs(x_choice->offset - x_selection.destination) <= abs(y_choice->offset - y_selection.destination)) { + y_choice = choose_y(x_choice->offset); + } else { + x_choice = choose_x(y_choice->offset); + } + } + } + if (x_choice.has_value() && !y_choice.has_value()) { + x_choice = choose_x(destination.y()); + } else if (y_choice.has_value() && !x_choice.has_value()) { + y_choice = choose_y(destination.x()); + } + } else if (snaps_x) { + x_choice = choose_x(destination.y()); + } else if (snaps_y) { + y_choice = choose_y(destination.x()); + } + + auto x_offset = x_choice.map([](auto const& choice) { return choice.offset; }); + auto y_offset = y_choice.map([](auto const& choice) { return choice.offset; }); + + auto snap_destination = snap_destination_for(destination, x_offset, y_offset, { snaps_x, snaps_y }); + if (x_offset.has_value()) + snap_destination.snapped_areas.x = snap_areas_at_offset(candidates.x_candidates, *x_offset, snap_destination.position.y()); + if (y_offset.has_value()) + snap_destination.snapped_areas.y = snap_areas_at_offset(candidates.y_candidates, *y_offset, snap_destination.position.x()); + return snap_destination; +} + +static bool snap_area_contains_node(SnapAreaReference const& area, DOM::Node const& node) +{ + // A box generated by a pseudo-element has no content of its own that could be focused or targeted. + if (!area.element || area.pseudo_element.has_value()) + return false; + return area.element->is_inclusive_ancestor_of(node); +} + +static CSSPixels resnap_offset_for_candidate(SnapPositionCandidate const& candidate, CSSPixels axis_current_offset) +{ + // A scroll container resting at one of its snapped area's valid snap positions is still snapped to that area and + // stays where it is. + if (candidate_has_snap_position_at(candidate, axis_current_offset)) + return axis_current_offset; + return candidate.offset; +} + +// The box each axis is snapped to, as an index into that axis's candidate list. +struct SnappedAxisBoxes { + Optional x_candidate; + Optional y_candidate; +}; + +// https://drafts.csswg.org/css-scroll-snap-1/#multiple-aligned-snap-areas +// When snapping to a scroll position that is aligned with multiple scroll snap areas, the following algorithm procedure +// is used to determined which box is snapped on the block and inline axes for a particular scroll container: +// NB: Every step treats the block and inline lists alike, so the steps are carried out on the physical axes directly. +static SnappedAxisBoxes select_between_multiple_aligned_snap_areas(SnapAxisCandidates const& candidates, ResnapSelection const& selection, CSSPixelPoint scroll_position) +{ + // 1. Let scroll position be the scroll position of the scroll container + // NB: The scroll position is the offset the content change left the container resting at. + + // 2. Let inline be the set of boxes whose scroll snap areas are aligned at this scroll position in the inline axis. + // 3. Let block be the set of boxes whose scroll snap areas are aligned at this scroll position in the block axis. + // AD-HOC: Only the snap areas the container was snapped to before the content change take part, since a re-snap + // must return the container to those same areas rather than to whichever areas the change left aligned. An + // area whose snap position would leave it outside the snapport in the other axis no longer offers a valid + // snap position and does not take part either. + // NB: A box is held as the index of its snap position candidate in its axis's candidate list, which holds its + // candidates in tree order. + auto aligned_boxes = [](Vector const& axis_candidates, Vector const& snapped_areas, CSSPixels cross_axis_offset) { + Vector boxes; + for (size_t candidate_index = 0; candidate_index < axis_candidates.size(); ++candidate_index) { + auto const& candidate = axis_candidates[candidate_index]; + if (!candidate.area.element || !snapped_areas.contains_slow(candidate.area)) + continue; + if (!snap_area_is_visible_at_cross_axis_offset(candidate, cross_axis_offset)) + continue; + boxes.append(candidate_index); + } + return boxes; + }; + auto x_boxes = aligned_boxes(candidates.x_candidates, selection.snapped_areas.x, scroll_position.y()); + auto y_boxes = aligned_boxes(candidates.y_candidates, selection.snapped_areas.y, scroll_position.x()); + + // 4. For each list of block and inline: + auto remove_superseded_boxes = [&](Vector& boxes, Vector const& axis_candidates) { + auto keep_only_boxes_matching = [&](auto const& predicate) { + if (!any_of(boxes, [&](size_t candidate_index) { return predicate(axis_candidates[candidate_index].area); })) + return false; + boxes.remove_all_matching([&](size_t candidate_index) { return !predicate(axis_candidates[candidate_index].area); }); + return true; + }; + + // 1. If list contains one or more boxes that are focused or have a focused descendant, remove all other boxes + // from list + bool kept_focused_boxes = selection.focused_node && keep_only_boxes_matching([&](SnapAreaReference const& area) { return snap_area_contains_node(area, *selection.focused_node); }); + + // 2. Else if list contains one or more boxes that are targetted or have a targetted descendant, remove all + // other boxes from list. + if (!kept_focused_boxes && selection.targeted_element) + keep_only_boxes_matching([&](SnapAreaReference const& area) { return snap_area_contains_node(area, *selection.targeted_element); }); + + // 3. For each box in list: + // 1. Remove any box from list which is an ancestor of box. + auto boxes_before_ancestor_removal = boxes; + boxes.remove_all_matching([&](size_t candidate_index) { + auto const& area = axis_candidates[candidate_index].area; + if (area.pseudo_element.has_value()) + return false; + return any_of(boxes_before_ancestor_removal, [&](size_t other_index) { + auto const& other_area = axis_candidates[other_index].area; + return other_area.element != area.element && area.element->is_inclusive_ancestor_of(*other_area.element); + }); + }); + }; + remove_superseded_boxes(x_boxes, candidates.x_candidates); + remove_superseded_boxes(y_boxes, candidates.y_candidates); + + auto boxes_contain_area = [](Vector const& boxes, Vector const& axis_candidates, SnapAreaReference const& area) { + return any_of(boxes, [&](size_t candidate_index) { return axis_candidates[candidate_index].area == area; }); + }; + bool axis_sets_overlap = any_of(x_boxes, [&](size_t candidate_index) { + return boxes_contain_area(y_boxes, candidates.y_candidates, candidates.x_candidates[candidate_index].area); + }); + // 5. If inline and block are overlapping sets: + if (axis_sets_overlap) { + // 1. Replace inline with the intersection of inline and block. + x_boxes.remove_all_matching([&](size_t candidate_index) { + return !boxes_contain_area(y_boxes, candidates.y_candidates, candidates.x_candidates[candidate_index].area); + }); + + // 2. Replace block with the intersection of inline and block. + // NB: The intersection is unchanged by the step before it, so the narrowed list gives the same result the + // original one would. + y_boxes.remove_all_matching([&](size_t candidate_index) { + return !boxes_contain_area(x_boxes, candidates.x_candidates, candidates.y_candidates[candidate_index].area); + }); + } + + // 6. Select the first element in tree order from inline as the snapped inline axis box. + // 7. Select the first element in tree order from block as the snapped block axis box. + return { + .x_candidate = x_boxes.is_empty() ? OptionalNone {} : Optional { x_boxes.first() }, + .y_candidate = y_boxes.is_empty() ? OptionalNone {} : Optional { y_boxes.first() }, + }; +} + +SnapDestination select_resnap_destination(Paintable const& snap_container, CSSPixelPoint current_offset, ResnapSelection const& selection) +{ + auto snap_axes = snap_axes_of_scroll_container(snap_container); + if (snap_axes.is_empty()) + return { current_offset }; + + // NB: A container that was not snapped before the change re-snaps the way a fresh scroll to the current position + // would. + if (selection.snapped_areas.is_empty()) + return adjust_scroll_destination_for_snapping(snap_container, current_offset); + + auto collection = collect_snap_position_candidates(snap_container, snap_axes.x, snap_axes.y); + if (!collection.has_value()) + return { current_offset }; + + auto const& candidates = collection->candidates; + + auto [x_chosen_candidate, y_chosen_candidate] = select_between_multiple_aligned_snap_areas(candidates, selection, current_offset); + + if (!x_chosen_candidate.has_value() && !y_chosen_candidate.has_value()) + return adjust_scroll_destination_for_snapping(snap_container, current_offset); + + auto fallback_offset_for_axis = [&](Vector const& axis_candidates, CSSPixels axis_current_offset, CSSPixels axis_snapport_size, CSSPixels cross_axis_offset) { + SnapAxisSelection axis_selection { + .destination = axis_current_offset, + .start = axis_current_offset, + .direction = 0, + .starting_positions_boundary = {}, + }; + auto choice = choose_snap_offset_for_axis(axis_candidates, axis_selection, axis_snapport_size, collection->snap_type.strictness, cross_axis_offset); + return choice.map([](auto const& axis_choice) { return axis_choice.offset; }); + }; + + // An axis whose snapped areas are all gone re-snaps afresh, from the snap positions reachable while the other + // axis follows its own snapped area. + Optional x_offset; + Optional y_offset; + if (x_chosen_candidate.has_value()) + x_offset = resnap_offset_for_candidate(candidates.x_candidates[*x_chosen_candidate], current_offset.x()); + if (y_chosen_candidate.has_value()) + y_offset = resnap_offset_for_candidate(candidates.y_candidates[*y_chosen_candidate], current_offset.y()); + if (snap_axes.x && !x_chosen_candidate.has_value()) + x_offset = fallback_offset_for_axis(candidates.x_candidates, current_offset.x(), collection->x_geometry.snapport_size, y_offset.value_or(current_offset.y())); + if (snap_axes.y && !y_chosen_candidate.has_value()) + y_offset = fallback_offset_for_axis(candidates.y_candidates, current_offset.y(), collection->y_geometry.snapport_size, x_offset.value_or(current_offset.x())); + + // https://drafts.csswg.org/css-scroll-snap-1/#re-snap + // If it is not possible to snap to both (e.g. if snapping to one resulted in the other being offscreen), it must + // prefer the focused box, followed by the targeted box, followed by the block axis if neither box is focused or + // targeted. + // NB: The preferred box is snapped in both axes: the other axis takes the box's own snap position when it defines + // one, and otherwise re-snaps among the positions at which the preferred box remains visible. + if (x_offset.has_value() && y_offset.has_value() && !chosen_offsets_are_mutually_visible(candidates, *x_offset, *y_offset)) { + auto const* style_source = style_source_for_snap_container(snap_container); + bool block_axis_is_y = style_source->writing_mode() == CSS::WritingMode::HorizontalTb; + + struct ResnapAxis { + Vector const& candidates; + Optional& chosen_candidate; + Optional& offset; + Optional area; + CSSPixels current_offset; + CSSPixels snapport_size; + }; + auto x_area = x_chosen_candidate.map([&](size_t candidate_index) { return candidates.x_candidates[candidate_index].area; }); + auto y_area = y_chosen_candidate.map([&](size_t candidate_index) { return candidates.y_candidates[candidate_index].area; }); + ResnapAxis x_axis { candidates.x_candidates, x_chosen_candidate, x_offset, move(x_area), current_offset.x(), collection->x_geometry.snapport_size }; + ResnapAxis y_axis { candidates.y_candidates, y_chosen_candidate, y_offset, move(y_area), current_offset.y(), collection->y_geometry.snapport_size }; + + auto area_contains_focus = [&](Optional const& area) { + return area.has_value() && selection.focused_node && snap_area_contains_node(*area, *selection.focused_node); + }; + auto area_contains_target = [&](Optional const& area) { + return area.has_value() && selection.targeted_element && snap_area_contains_node(*area, *selection.targeted_element); + }; + + bool preferred_axis_is_x; + if (area_contains_focus(x_axis.area) || area_contains_focus(y_axis.area)) { + preferred_axis_is_x = area_contains_focus(x_axis.area); + } else if (area_contains_target(x_axis.area) || area_contains_target(y_axis.area)) { + preferred_axis_is_x = area_contains_target(x_axis.area); + } else if (x_axis.area.has_value() != y_axis.area.has_value()) { + preferred_axis_is_x = x_axis.area.has_value(); + } else { + preferred_axis_is_x = !block_axis_is_y; + } + auto const& preferred_axis = preferred_axis_is_x ? x_axis : y_axis; + auto& other_axis = preferred_axis_is_x ? y_axis : x_axis; + + auto preferred_area_candidate_in_other_axis = other_axis.candidates.find_if([&](auto const& candidate) { return candidate.area == *preferred_axis.area; }); + if (preferred_area_candidate_in_other_axis != other_axis.candidates.end()) { + other_axis.offset = resnap_offset_for_candidate(*preferred_area_candidate_in_other_axis, other_axis.current_offset); + other_axis.chosen_candidate = preferred_area_candidate_in_other_axis.index(); + } else { + other_axis.offset = fallback_offset_for_axis(other_axis.candidates, other_axis.current_offset, other_axis.snapport_size, *preferred_axis.offset); + other_axis.chosen_candidate = {}; + } + } + + auto snap_destination = snap_destination_for(current_offset, x_offset, y_offset, snap_axes); + + // An axis a re-snap left where it was selected nothing, so an area that happens to have become aligned at the + // kept position does not take the place of the areas the container was snapped to. + auto record_snapped_areas = [&](Optional const& chosen_candidate, Vector const& axis_candidates, Vector const& previously_snapped_areas, CSSPixels axis_offset, CSSPixels axis_current_offset, CSSPixels cross_axis_offset) { + auto areas = snap_areas_at_offset(axis_candidates, axis_offset, cross_axis_offset); + if (chosen_candidate.has_value() && axis_offset == axis_current_offset) { + auto const& chosen_area = axis_candidates[*chosen_candidate].area; + areas.remove_all_matching([&](auto const& area) { return area != chosen_area && !previously_snapped_areas.contains_slow(area); }); + } + return areas; + }; + if (x_offset.has_value()) + snap_destination.snapped_areas.x = record_snapped_areas(x_chosen_candidate, candidates.x_candidates, selection.snapped_areas.x, *x_offset, current_offset.x(), snap_destination.position.y()); + if (y_offset.has_value()) + snap_destination.snapped_areas.y = record_snapped_areas(y_chosen_candidate, candidates.y_candidates, selection.snapped_areas.y, *y_offset, current_offset.y(), snap_destination.position.x()); + return snap_destination; +} + +// The largest share of a momentum delta that the delta after it may keep for the momentum to be considered decaying. +static constexpr double maximum_momentum_decay_share = 0.96; + +// The share a momentum delta keeps of the one before it is treated as no larger than this, so that momentum which +// barely decays is predicted to travel a hundred times the distance of its latest delta rather than forever. +static constexpr double maximum_slow_momentum_decay_share = 0.99; + +// The number of consecutively smaller momentum deltas after which momentum that decays only slowly is predicted from +// anyway. +static constexpr u32 decaying_deltas_before_slow_decay_is_predicted_from = 3; + +void MomentumFlingEstimator::reset() +{ + m_previous_momentum_delta = {}; + m_consecutively_decaying_momentum_deltas = 0; +} + +Optional MomentumFlingEstimator::estimate_remaining_displacement(CSSPixelPoint momentum_delta) +{ + auto previous_momentum_delta = m_previous_momentum_delta; + m_previous_momentum_delta = momentum_delta; + + // The share the delta keeps of the one before it is what the momentum decays by, so the first delta of a flick + // says nothing about where it is headed. + if (!previous_momentum_delta.has_value()) + return {}; + auto distance = AK::hypot(momentum_delta.x().to_double(), momentum_delta.y().to_double()); + auto previous_distance = AK::hypot(previous_momentum_delta->x().to_double(), previous_momentum_delta->y().to_double()); + if (previous_distance <= 0) + return {}; + auto decay_share = distance / previous_distance; + + if (decay_share < 1) { + ++m_consecutively_decaying_momentum_deltas; + } else { + m_consecutively_decaying_momentum_deltas = 0; + } + + auto momentum_is_decaying = decay_share < maximum_momentum_decay_share + || (m_consecutively_decaying_momentum_deltas >= decaying_deltas_before_slow_decay_is_predicted_from && decay_share < 1); + if (!momentum_is_decaying) + return {}; + + // Each delta keeps the same share of the one before it, so the deltas still to come sum to the delta given + // divided by the share it loses each time. + auto remaining_distance_factor = 1 / (1 - min(decay_share, maximum_slow_momentum_decay_share)); + return CSSPixelPoint { + CSSPixels::nearest_value_for(momentum_delta.x().to_double() * remaining_distance_factor), + CSSPixels::nearest_value_for(momentum_delta.y().to_double() * remaining_distance_factor), + }; +} + +} diff --git a/Libraries/LibWeb/Painting/ScrollSnap.h b/Libraries/LibWeb/Painting/ScrollSnap.h new file mode 100644 index 0000000000000..748f3fe6cf237 --- /dev/null +++ b/Libraries/LibWeb/Painting/ScrollSnap.h @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Web::DOM { + +class Element; +class Node; + +} + +namespace Web::Painting { + +class Paintable; + +// The element a snap area belongs to, which identifies the area across relayouts. The reference is weak: an area +// removed from the document is no longer a snap area the container can be returned to. +struct SnapAreaReference { + GC::Weak element; + Optional pseudo_element; + + // Two references to areas that have both gone away compare equal, which no remembered area is ever tested against. + bool operator==(SnapAreaReference const& other) const + { + return element.ptr() == other.element.ptr() && pseudo_element == other.pseudo_element; + } +}; + +// https://drafts.csswg.org/css-scroll-snap-1/#scroll-types +struct SnapSelectionStrategy { + enum class Type : u8 { + // An absolute scroll, or any other operation with only an intended end position. + EndPosition, + // A relative scroll with only an intended direction, such as a mouse wheel step or an arrow key press. + Direction, + // A relative scroll with both an intended direction and end position, such as scrollBy(). + EndPositionAndDirection, + }; + + Type type { Type::EndPosition }; + // The scroll offset the operation travels from; a snap position with `scroll-snap-stop: always` must not be + // passed over on the way from there to the selected snap position. + Optional start_offset {}; + // The net offset change the operation's input produced; an axis the operation did not travel in selects no snap + // position. + CSSPixelPoint displacement {}; + // Snap positions short of this offset in the direction of travel are not selected; it defaults to the start + // offset. + Optional starting_positions_boundary {}; +}; + +// The displacement the momentum of a flick has left to travel, estimated from the deltas that momentum has produced +// so far. +class WEB_API MomentumFlingEstimator { +public: + void reset(); + + // The displacement left to travel, including the delta given; momentum that has not yet decayed far enough to + // tell where it is headed reports no estimate. + Optional estimate_remaining_displacement(CSSPixelPoint momentum_delta); + +private: + Optional m_previous_momentum_delta; + u32 m_consecutively_decaying_momentum_deltas { 0 }; +}; + +struct SnapAxes { + bool x { false }; + bool y { false }; + + bool is_empty() const { return !x && !y; } +}; + +WEB_API SnapAxes snap_axes_of_scroll_container(Paintable const& snap_container); + +WEB_API bool is_scroll_snap_container(Paintable const&); + +// The snap areas a scroll container is snapped to in each axis, so that it can be re-snapped to those same snap areas +// after a content change. +struct SnappedAreas { + Vector x; + Vector y; + + bool is_empty() const { return x.is_empty() && y.is_empty(); } +}; + +struct SnapDestination { + CSSPixelPoint position; + // Whether a snap position was selected in each axis; an axis whose snap positions are all ineligible for the + // scroll keeps the destination it was given. + bool snapped_x { false }; + bool snapped_y { false }; + // Whether snap position selection ran in each axis. An axis the scroll did not travel in is not evaluated, and + // whatever snap area the container is snapped to there remains snapped. + bool evaluated_x { false }; + bool evaluated_y { false }; + SnappedAreas snapped_areas {}; +}; + +WEB_API SnapDestination adjust_scroll_destination_for_snapping(Paintable const& snap_container, CSSPixelPoint destination, SnapSelectionStrategy const& strategy = {}); + +struct ResnapSelection { + SnappedAreas const& snapped_areas; + GC::Ptr focused_node; + GC::Ptr targeted_element; +}; + +WEB_API SnapDestination select_resnap_destination(Paintable const& snap_container, CSSPixelPoint current_offset, ResnapSelection const&); + +} diff --git a/Libraries/LibWeb/Painting/Scrollbar.cpp b/Libraries/LibWeb/Painting/Scrollbar.cpp index 0e53573a00f7d..0eae3c0b35ed2 100644 --- a/Libraries/LibWeb/Painting/Scrollbar.cpp +++ b/Libraries/LibWeb/Painting/Scrollbar.cpp @@ -148,6 +148,13 @@ bool Scrollbar::scroll_to_mouse_position(CSSPixelPoint position) auto new_scroll_offset = paintable_box->scroll_offset(); new_scroll_offset.set_primary_offset_for_orientation(orientation, scroll_position_in_pixels); + + // https://drafts.csswg.org/css-scroll-snap-1/#scroll-types + // Common examples of absolute scrolls include: + // manipulating the scrollbar "thumb" explicitly + if (auto navigable = paintable_box->document().navigable()) + navigable->note_user_scroll_input_intent(Painting::SnapSelectionStrategy::Type::EndPosition); + paintable_box->set_scroll_offset_from_user_input(new_scroll_offset); return true; } diff --git a/Libraries/LibWeb/Rust/src/css/computed_value_types.rs b/Libraries/LibWeb/Rust/src/css/computed_value_types.rs index 3e3348cd3f952..0a08c2106582f 100644 --- a/Libraries/LibWeb/Rust/src/css/computed_value_types.rs +++ b/Libraries/LibWeb/Rust/src/css/computed_value_types.rs @@ -295,6 +295,11 @@ pub struct MiscResetValues { pub touch_action_allow_pinch_zoom: bool, pub touch_action_allow_other: bool, pub scroll_behavior: u8, + pub scroll_snap_align_block: u8, + pub scroll_snap_align_inline: u8, + pub scroll_snap_stop: u8, + pub scroll_snap_axis: u8, + pub scroll_snap_strictness: u8, pub scrollbar_gutter: u8, pub scrollbar_width: u8, pub shape_image_threshold: f64, diff --git a/Libraries/LibWeb/Rust/src/css/computed_values.rs b/Libraries/LibWeb/Rust/src/css/computed_values.rs index 5ec383979a8a7..a90bf4d33a969 100644 --- a/Libraries/LibWeb/Rust/src/css/computed_values.rs +++ b/Libraries/LibWeb/Rust/src/css/computed_values.rs @@ -587,6 +587,11 @@ impl_computed_payload_clone_and_eq!(MiscResetValues { touch_action_allow_pinch_zoom, touch_action_allow_other, scroll_behavior, + scroll_snap_align_block, + scroll_snap_align_inline, + scroll_snap_stop, + scroll_snap_axis, + scroll_snap_strictness, scrollbar_gutter, scrollbar_width, shape_image_threshold, @@ -1730,7 +1735,7 @@ enum GroupFieldPoke { Data(u32, *const crate::css::style_value::StyleValueData), } -const MAX_GROUP_FIELD_COUNT: usize = 32; +const MAX_GROUP_FIELD_COUNT: usize = 40; struct GroupFieldPokes { entries: [std::mem::MaybeUninit; MAX_GROUP_FIELD_COUNT], @@ -2970,6 +2975,11 @@ impl MiscResetValues { touch_action_allow_pinch_zoom: true, touch_action_allow_other: true, scroll_behavior: 0, + scroll_snap_align_block: crate::css::css_enums::scroll_snap_align::NONE, + scroll_snap_align_inline: crate::css::css_enums::scroll_snap_align::NONE, + scroll_snap_stop: crate::css::css_enums::scroll_snap_stop::NORMAL, + scroll_snap_axis: crate::css::css_enums::scroll_snap_axis::BOTH, + scroll_snap_strictness: crate::css::css_enums::scroll_snap_strictness::NONE, scrollbar_gutter: 0, scrollbar_width: 0, shape_image_threshold: 0.0, diff --git a/Libraries/LibWeb/Rust/src/css/table_group_builder.rs b/Libraries/LibWeb/Rust/src/css/table_group_builder.rs index c3b2648b85a33..094895bd01110 100644 --- a/Libraries/LibWeb/Rust/src/css/table_group_builder.rs +++ b/Libraries/LibWeb/Rust/src/css/table_group_builder.rs @@ -135,7 +135,7 @@ struct EffectiveValues<'a> { override_values: &'a [*const c_void], } -const MAX_GROUP_FIELD_COUNT: usize = 32; +const MAX_GROUP_FIELD_COUNT: usize = 40; struct GroupValueEntries { entries: [std::mem::MaybeUninit; MAX_GROUP_FIELD_COUNT], @@ -2462,6 +2462,52 @@ unsafe fn build_misc_reset_group( unreachable!("a computed scrollbar-gutter is a scrollbar-gutter value"); }; + let scroll_snap_alignment = |data: &StyleValueData| { + keyword_of(data) + .and_then(crate::css::css_enums::keyword_to_scroll_snap_align) + .expect("a computed scroll-snap-align keyword maps to its enum") + }; + let (scroll_snap_align_block, scroll_snap_align_inline) = match values.value(property_id::SCROLL_SNAP_ALIGN) { + Some(StyleValueData::ValueList { values: list, .. }) => { + let components = list.as_slice(); + ( + scroll_snap_alignment(components[0].data()), + scroll_snap_alignment(components[1].data()), + ) + } + Some(data) => { + let alignment = scroll_snap_alignment(data); + (alignment, alignment) + } + None => unreachable!("the table holds scroll-snap-align"), + }; + + let (scroll_snap_axis, scroll_snap_strictness) = match values.value(property_id::SCROLL_SNAP_TYPE) { + Some(StyleValueData::ValueList { values: list, .. }) => { + let components = list.as_slice(); + ( + keyword_of(components[0].data()) + .and_then(crate::css::css_enums::keyword_to_scroll_snap_axis) + .expect("a computed scroll-snap-type axis keyword maps to its enum"), + keyword_of(components[1].data()) + .and_then(crate::css::css_enums::keyword_to_scroll_snap_strictness) + .expect("a computed scroll-snap-type strictness keyword maps to its enum"), + ) + } + Some(data) => match keyword_of(data).expect("a computed single-value scroll-snap-type is a keyword") { + keyword::NONE => ( + crate::css::css_enums::scroll_snap_axis::BOTH, + crate::css::css_enums::scroll_snap_strictness::NONE, + ), + code => ( + crate::css::css_enums::keyword_to_scroll_snap_axis(code) + .expect("a computed single-keyword scroll-snap-type is none, x, y, block, inline or both"), + crate::css::css_enums::scroll_snap_strictness::PROXIMITY, + ), + }, + None => unreachable!("the table holds scroll-snap-type"), + }; + let outline_offset_data = values .value(property_id::OUTLINE_OFFSET) .expect("the table holds outline-offset"); @@ -2520,6 +2566,10 @@ unsafe fn build_misc_reset_group( payload.touch_action_allow_down = allow[3]; payload.touch_action_allow_pinch_zoom = allow[4]; payload.touch_action_allow_other = allow[5]; + payload.scroll_snap_align_block = scroll_snap_align_block; + payload.scroll_snap_align_inline = scroll_snap_align_inline; + payload.scroll_snap_axis = scroll_snap_axis; + payload.scroll_snap_strictness = scroll_snap_strictness; payload.scrollbar_gutter = *scrollbar_gutter; payload.shape_margin = retained(property_id::SHAPE_MARGIN); payload.shape_outside = retained(property_id::SHAPE_OUTSIDE); diff --git a/Libraries/LibWeb/Rust/src/painting/display_list/commands.rs b/Libraries/LibWeb/Rust/src/painting/display_list/commands.rs index 636585147f31a..27e94320364cc 100644 --- a/Libraries/LibWeb/Rust/src/painting/display_list/commands.rs +++ b/Libraries/LibWeb/Rust/src/painting/display_list/commands.rs @@ -1258,6 +1258,8 @@ pub struct CompositorScrollNode { pub is_viewport: bool, pub can_be_wheel_scrolled_horizontally: bool, pub can_be_wheel_scrolled_vertically: bool, + pub snaps_scroll_position_horizontally: bool, + pub snaps_scroll_position_vertically: bool, } ffi_bytes_fields!(CompositorScrollNode { document_id, @@ -1271,7 +1273,9 @@ ffi_bytes_fields!(CompositorScrollNode { pseudo_element_type, is_viewport, can_be_wheel_scrolled_horizontally, - can_be_wheel_scrolled_vertically + can_be_wheel_scrolled_vertically, + snaps_scroll_position_horizontally, + snaps_scroll_position_vertically }); impl DisplayListCommand for CompositorScrollNode { diff --git a/Libraries/LibWeb/Rust/src/painting/ffi.rs b/Libraries/LibWeb/Rust/src/painting/ffi.rs index 48cacdeb285e2..4bddf4c756635 100644 --- a/Libraries/LibWeb/Rust/src/painting/ffi.rs +++ b/Libraries/LibWeb/Rust/src/painting/ffi.rs @@ -453,6 +453,47 @@ pub unsafe extern "C" fn layout_arena_assign_accumulated_visual_contexts( }) } +/// # Safety +/// +/// `arena` must be a live handle from `layout_arena_create`, used on the +/// document thread; `out_matrix` and `out_origin` must hold 16 and 2 floats. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn layout_arena_compute_css_transform( + arena: *mut c_void, + paintable: PaintableSlotId, + callbacks: FfiVisualContextHostCallbacks, + pixel_ratio: f64, + out_matrix: *mut f32, + out_origin: *mut f32, +) -> bool { + abort_on_panic(|| { + let arena = unsafe { arena_from_handle(arena) }; + let paintables = arena.paintables().borrow(); + if !paintables.is_live(paintable) { + return false; + } + let Some((transform, _is_invertible)) = crate::painting::visual_context::node_values::compute_transform( + arena, + &paintables, + &callbacks, + paintable, + pixel_ratio, + ) else { + return false; + }; + for (index, value) in transform.matrix.elements.into_iter().flatten().enumerate() { + // SAFETY: the caller warrants 16 floats behind out_matrix. + unsafe { out_matrix.add(index).write(value) }; + } + // SAFETY: the caller warrants 2 floats behind out_origin. + unsafe { + out_origin.write(transform.origin.x); + out_origin.add(1).write(transform.origin.y); + } + true + }) +} + /// # Safety /// /// `arena` must be a live handle from `layout_arena_create`, used on the document thread. diff --git a/Libraries/LibWeb/Rust/src/painting/host/paint.rs b/Libraries/LibWeb/Rust/src/painting/host/paint.rs index a4e5d3b0fc702..33872ea115111 100644 --- a/Libraries/LibWeb/Rust/src/painting/host/paint.rs +++ b/Libraries/LibWeb/Rust/src/painting/host/paint.rs @@ -242,6 +242,8 @@ pub struct FfiAsyncScrollFacts { pub scroll_node_kind: FfiScrollNodeKind, pub scrollable_node_id: i64, pub pseudo_element_type: u8, + pub snaps_scroll_position_horizontally: bool, + pub snaps_scroll_position_vertically: bool, pub inside_blocking_wheel_event_handler: bool, pub records_viewport_scrollbars: bool, pub viewport_scrollbars: [FfiViewportScrollbarFacts; 2], diff --git a/Libraries/LibWeb/Rust/src/painting/record/async_scroll_metadata.rs b/Libraries/LibWeb/Rust/src/painting/record/async_scroll_metadata.rs index 67bb7a1ef730a..09b3c284e4764 100644 --- a/Libraries/LibWeb/Rust/src/painting/record/async_scroll_metadata.rs +++ b/Libraries/LibWeb/Rust/src/painting/record/async_scroll_metadata.rs @@ -271,6 +271,8 @@ impl PaintRecorder<'_> { is_viewport, can_be_wheel_scrolled_horizontally: hit_test_facts.could_be_scrolled_horizontally, can_be_wheel_scrolled_vertically: hit_test_facts.could_be_scrolled_vertically, + snaps_scroll_position_horizontally: facts.snaps_scroll_position_horizontally, + snaps_scroll_position_vertically: facts.snaps_scroll_position_vertically, }); } diff --git a/Libraries/LibWeb/WebDriver/Actions.cpp b/Libraries/LibWeb/WebDriver/Actions.cpp index 97224d98d9e25..124c593f3642e 100644 --- a/Libraries/LibWeb/WebDriver/Actions.cpp +++ b/Libraries/LibWeb/WebDriver/Actions.cpp @@ -1336,7 +1336,10 @@ static ErrorOr dispatch_scroll_action(ActionObject::Scro // but the total scroll applied at the end of duration milliseconds must be delta x and delta y, and after each // increment the sum of the applied deltas must not be greater than delta x and delta y. auto position = browsing_context.page().css_to_device_point(coordinates); - browsing_context.page().handle_mousewheel(position, position, 0, 0, global_key_state.modifiers(), static_cast(action_object.delta_x), static_cast(action_object.delta_y)); + + // AD-HOC: A scroll action emulates a mouse wheel, so its deltas are stepwise wheel input. A snap container the + // action scrolls therefore ends at the snap position the input selects, rather than at the requested delta. + browsing_context.page().handle_mousewheel(position, position, 0, 0, global_key_state.modifiers(), static_cast(action_object.delta_x), static_cast(action_object.delta_y), WheelDeltaPrecision::Discrete); // 12. Return success with data null. return {}; diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index e1b5b9e9b87ce..e78c039d6ba0c 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -1000,12 +1000,12 @@ void Application::update_compositor_display_metadata(Web::Compositor::Compositor m_compositor_client->async_set_display_metadata(context_id, display_id, sanitized_display_refresh_rate(refresh_rate)); } -bool Application::send_async_scroll_to_compositor(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels) +bool Application::send_async_scroll_to_compositor(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels, Web::Compositor::SnapContainerHandling snap_container_handling) { if (!can_send_compositor_process_ipc(m_compositor_client)) return false; - auto result = m_compositor_client->try_async_scroll_by(context_id, position, delta_in_device_pixels); + auto result = m_compositor_client->try_async_scroll_by(context_id, position, delta_in_device_pixels, snap_container_handling); if (result.is_error()) return false; return result.release_value(); diff --git a/Libraries/LibWebView/Application.h b/Libraries/LibWebView/Application.h index a31303f9e3b51..a6ba26206845c 100644 --- a/Libraries/LibWebView/Application.h +++ b/Libraries/LibWebView/Application.h @@ -171,7 +171,7 @@ class WEBVIEW_API Application : public DevTools::DevToolsDelegate { ErrorOr try_register_compositor_context(WebContentClient&, Web::Compositor::CompositorContextId, Optional page_id); void update_compositor_viewport(Web::Compositor::CompositorContextId, Gfx::IntSize viewport_size, Web::Compositor::WindowResizingInProgress = Web::Compositor::WindowResizingInProgress::No); void update_compositor_display_metadata(Web::Compositor::CompositorContextId, Optional display_id, double refresh_rate); - bool send_async_scroll_to_compositor(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels); + bool send_async_scroll_to_compositor(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels, Web::Compositor::SnapContainerHandling); bool handle_mouse_event_in_compositor(Web::Compositor::CompositorContextId, Web::MouseEvent const&); bool handle_pinch_event_in_compositor(Web::Compositor::CompositorContextId, Web::PinchEvent const&); bool dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId, Web::MouseEvent const&); diff --git a/Libraries/LibWebView/CompositorConnection.cpp b/Libraries/LibWebView/CompositorConnection.cpp index c45f686e14c30..808e132211eda 100644 --- a/Libraries/LibWebView/CompositorConnection.cpp +++ b/Libraries/LibWebView/CompositorConnection.cpp @@ -179,12 +179,12 @@ void CompositorConnection::invalidate_wheel_event_listener_state(Web::Compositor async_invalidate_wheel_event_listener_state(context_id, generation); } -Web::Compositor::AsyncScrollEnqueueResult CompositorConnection::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking operation_tracking) +Web::Compositor::AsyncScrollEnqueueResult CompositorConnection::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling snap_container_handling, Web::Compositor::AsyncScrollOperationTracking operation_tracking) { if (!can_send_message_to_compositor()) return {}; - auto response = send_sync_but_allow_failure(context_id, document_id, position, delta, viewport_rect, operation_tracking); + auto response = send_sync_but_allow_failure(context_id, document_id, position, delta, viewport_rect, snap_container_handling, operation_tracking); if (!response) { did_lose_compositor(); return {}; @@ -192,12 +192,12 @@ Web::Compositor::AsyncScrollEnqueueResult CompositorConnection::async_scroll_by( return response->take_result(); } -Web::Compositor::AsyncScrollEnqueueResult CompositorConnection::smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) +Web::Compositor::AsyncScrollEnqueueResult CompositorConnection::smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind animation_kind) { if (!can_send_message_to_compositor()) return {}; - auto response = send_sync_but_allow_failure(context_id, stable_node_id, offset, viewport_rect, device_pixels_per_css_pixel); + auto response = send_sync_but_allow_failure(context_id, stable_node_id, offset, main_thread_offset, viewport_rect, device_pixels_per_css_pixel, animation_kind); if (!response) { did_lose_compositor(); return {}; diff --git a/Libraries/LibWebView/CompositorConnection.h b/Libraries/LibWebView/CompositorConnection.h index 55f8972c9aab2..0891d53de9404 100644 --- a/Libraries/LibWebView/CompositorConnection.h +++ b/Libraries/LibWebView/CompositorConnection.h @@ -54,8 +54,8 @@ class WEBVIEW_API CompositorConnection final void destroy_canvas_context(Web::Painting::CanvasId); Gfx::ShareableBitmap get_canvas_pixels(Web::Painting::CanvasId, Gfx::IntRect); void invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId, u64 generation); - Web::Compositor::AsyncScrollEnqueueResult async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking); - Web::Compositor::AsyncScrollEnqueueResult smooth_scroll_to(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel); + Web::Compositor::AsyncScrollEnqueueResult async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling, Web::Compositor::AsyncScrollOperationTracking); + Web::Compositor::AsyncScrollEnqueueResult smooth_scroll_to(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind); void cancel_smooth_scroll(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID); Web::Compositor::PendingAsyncScrollUpdates take_pending_async_scroll_updates(Web::Compositor::CompositorContextId); void viewport_size_updated(Web::Compositor::CompositorContextId, Gfx::IntSize, Web::Compositor::WindowResizingInProgress); diff --git a/Libraries/LibWebView/CompositorHostBase.cpp b/Libraries/LibWebView/CompositorHostBase.cpp index 7aec4c4be9eb5..a331475390e3b 100644 --- a/Libraries/LibWebView/CompositorHostBase.cpp +++ b/Libraries/LibWebView/CompositorHostBase.cpp @@ -256,17 +256,17 @@ void CompositorHostBase::invalidate_wheel_event_listener_state(Web::Compositor:: } Web::Compositor::AsyncScrollEnqueueResult CompositorHostBase::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID expected_document_id, Gfx::FloatPoint position, - Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking operation_tracking) + Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling snap_container_handling, Web::Compositor::AsyncScrollOperationTracking operation_tracking) { if (auto* connection = compositor_connection()) - return connection->async_scroll_by(context_id, expected_document_id, position, delta_in_device_pixels, viewport_rect, operation_tracking); + return connection->async_scroll_by(context_id, expected_document_id, position, delta_in_device_pixels, viewport_rect, snap_container_handling, operation_tracking); return {}; } -Web::Compositor::AsyncScrollEnqueueResult CompositorHostBase::smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) +Web::Compositor::AsyncScrollEnqueueResult CompositorHostBase::smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset_in_device_pixels, Gfx::FloatPoint main_thread_offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind animation_kind) { if (auto* connection = compositor_connection()) - return connection->smooth_scroll_to(context_id, stable_node_id, offset_in_device_pixels, viewport_rect, device_pixels_per_css_pixel); + return connection->smooth_scroll_to(context_id, stable_node_id, offset_in_device_pixels, main_thread_offset_in_device_pixels, viewport_rect, device_pixels_per_css_pixel, animation_kind); return {}; } diff --git a/Libraries/LibWebView/CompositorHostBase.h b/Libraries/LibWebView/CompositorHostBase.h index 59316975de7f2..622711a7e2073 100644 --- a/Libraries/LibWebView/CompositorHostBase.h +++ b/Libraries/LibWebView/CompositorHostBase.h @@ -30,8 +30,8 @@ class WEBVIEW_API CompositorHostBase : public Web::Compositor::CompositorHost { virtual void update_scroll_state(Web::Compositor::CompositorContextId, Web::Painting::ScrollStateSnapshot&&) override; virtual void invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId, u64 generation) override; virtual Web::Compositor::AsyncScrollEnqueueResult async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID expected_document_id, Gfx::FloatPoint position, - Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking) override; - virtual Web::Compositor::AsyncScrollEnqueueResult smooth_scroll_to(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) override; + Gfx::FloatPoint delta_in_device_pixels, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling, Web::Compositor::AsyncScrollOperationTracking) override; + virtual Web::Compositor::AsyncScrollEnqueueResult smooth_scroll_to(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset_in_device_pixels, Gfx::FloatPoint main_thread_offset_in_device_pixels, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind) override; virtual void cancel_smooth_scroll(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID) override; virtual Web::Compositor::PendingAsyncScrollUpdates take_pending_async_scroll_updates(Web::Compositor::CompositorContextId) override; virtual void viewport_size_updated(Web::Compositor::CompositorContextId, Gfx::IntSize, Web::Compositor::WindowResizingInProgress) override; diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index c97de97581e25..28a48c8d18622 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -695,7 +695,8 @@ void ViewImplementation::enqueue_input_event(Web::InputEvent event) auto delta_in_device_pixels = Gfx::FloatPoint { wheel_delta_x, wheel_delta_y }.scaled(device_pixels_per_css_pixel); dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI attempting compositor wheel bypass for page {} at {},{} device delta {},{}", m_client_state.page_index, position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y()); - if (client().send_async_scroll_to_compositor(m_client_state.page_index, position, delta_in_device_pixels)) + auto snap_container_handling = Web::Compositor::snap_container_handling_for(mouse_event->wheel_delta_precision, mouse_event->scroll_gesture_phase); + if (client().send_async_scroll_to_compositor(m_client_state.page_index, position, delta_in_device_pixels, snap_container_handling)) mouse_event->async_scroll_performed_default_action = true; dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor wheel bypass result for page {}: {}", m_client_state.page_index, mouse_event->async_scroll_performed_default_action ? "accepted"sv : "rejected"sv); diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index ba63c74546bd7..9cb349e8818ef 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -409,11 +409,11 @@ void WebContentClient::notify_all_views_of_crash() } } -bool WebContentClient::send_async_scroll_to_compositor(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels) +bool WebContentClient::send_async_scroll_to_compositor(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels, Web::Compositor::SnapContainerHandling snap_container_handling) { auto timer = Core::ElapsedTimer::start_new(Core::TimerType::Precise); - auto handled = Application::the().send_async_scroll_to_compositor(compositor_context_id_for_page(page_id), position, delta_in_device_pixels); + auto handled = Application::the().send_async_scroll_to_compositor(compositor_context_id_for_page(page_id), position, delta_in_device_pixels, snap_container_handling); dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor IPC async_scroll_by page {} returned {} in {} us", page_id, handled, timer.elapsed_time().to_microseconds()); diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index 28715c6160f55..c5a51f458798c 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -105,7 +105,7 @@ class WEBVIEW_API WebContentClient final void notify_compositor_process_reconnected(Badge); Web::Compositor::CompositorContextId compositor_context_id_for_page(u64 page_id); Optional page_id_for_compositor_context_id(Web::Compositor::CompositorContextId) const; - bool send_async_scroll_to_compositor(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels); + bool send_async_scroll_to_compositor(u64 page_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels, Web::Compositor::SnapContainerHandling); bool handle_mouse_event_in_compositor(u64 page_id, Web::MouseEvent const&); bool handle_pinch_event_in_compositor(u64 page_id, Web::PinchEvent const&); void dispatch_mouse_event_to_web_content(u64 page_id, Web::MouseEvent const&); diff --git a/Services/Compositor/CompositorControlServer.ipc b/Services/Compositor/CompositorControlServer.ipc index 7690ebac0f7bc..fa5895045dbbc 100644 --- a/Services/Compositor/CompositorControlServer.ipc +++ b/Services/Compositor/CompositorControlServer.ipc @@ -19,7 +19,7 @@ endpoint CompositorControlServer handle_mouse_event(Web::Compositor::CompositorContextId context_id, Web::MouseEvent event) => (bool handled) dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId context_id, Web::MouseEvent event) => (bool dispatched) handle_pinch_event(Web::Compositor::CompositorContextId context_id, Web::PinchEvent event) => (bool handled) - async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels) => (bool handled) + async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels, Web::Compositor::SnapContainerHandling snap_container_handling) => (bool handled) presented_bitmap_ready_to_paint(Web::Compositor::CompositorContextId context_id, i32 bitmap_id) =| set_client_gpu_presentation_capability(bool supported, u64 adapter_luid) =| crash() =| diff --git a/Services/Compositor/CompositorState.cpp b/Services/Compositor/CompositorState.cpp index 810cdc17f450c..39259eb996a8c 100644 --- a/Services/Compositor/CompositorState.cpp +++ b/Services/Compositor/CompositorState.cpp @@ -417,7 +417,7 @@ bool CompositorState::handle_pinch_event(Web::Compositor::CompositorContextId co return apply_context_update_result(context_id, *context, context->handle_pinch_event(event)); } -Web::Compositor::AsyncScrollEnqueueResult CompositorState::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID expected_document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking operation_tracking) +Web::Compositor::AsyncScrollEnqueueResult CompositorState::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID expected_document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling snap_container_handling, Web::Compositor::AsyncScrollOperationTracking operation_tracking) { if (!m_async_scrolling_enabled) return {}; @@ -425,13 +425,13 @@ Web::Compositor::AsyncScrollEnqueueResult CompositorState::async_scroll_by(Web:: auto* context = context_if_present(context_id); VERIFY(context); - auto result = context->async_scroll_by(expected_document_id, position, delta, viewport_rect, operation_tracking); + auto result = context->async_scroll_by(expected_document_id, position, delta, viewport_rect, snap_container_handling, operation_tracking); if (result.frame_to_present.has_value()) schedule_present_frame(context_id, *context, *result.frame_to_present); return result.enqueue_result; } -Web::Compositor::AsyncScrollEnqueueResult CompositorState::smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) +Web::Compositor::AsyncScrollEnqueueResult CompositorState::smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind animation_kind) { if (!m_async_scrolling_enabled) return {}; @@ -439,7 +439,7 @@ Web::Compositor::AsyncScrollEnqueueResult CompositorState::smooth_scroll_to(Web: auto* context = context_if_present(context_id); VERIFY(context); - auto result = context->smooth_scroll_to(stable_node_id, offset, viewport_rect, device_pixels_per_css_pixel); + auto result = context->smooth_scroll_to(stable_node_id, offset, main_thread_offset, viewport_rect, device_pixels_per_css_pixel, animation_kind); if (result.frame_to_present.has_value()) schedule_present_frame(context_id, *context, *result.frame_to_present); return result.enqueue_result; @@ -453,7 +453,7 @@ void CompositorState::cancel_smooth_scroll(Web::Compositor::CompositorContextId context->cancel_smooth_scroll(stable_node_id); } -bool CompositorState::async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta) +bool CompositorState::async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Web::Compositor::SnapContainerHandling snap_container_handling) { if (!m_async_scrolling_enabled) return false; @@ -462,7 +462,7 @@ bool CompositorState::async_scroll_by(Web::Compositor::CompositorContextId conte if (!context) return false; - return apply_context_update_result(context_id, *context, context->async_scroll_by(position, delta)); + return apply_context_update_result(context_id, *context, context->async_scroll_by(position, delta, snap_container_handling)); } Web::Compositor::PendingAsyncScrollUpdates CompositorState::take_pending_async_scroll_updates(Web::Compositor::CompositorContextId context_id) diff --git a/Services/Compositor/CompositorState.h b/Services/Compositor/CompositorState.h index 5df47ff6dfc45..261c6c0450fae 100644 --- a/Services/Compositor/CompositorState.h +++ b/Services/Compositor/CompositorState.h @@ -96,10 +96,10 @@ class CompositorState final : public RefCounted { bool handle_mouse_event(Web::Compositor::CompositorContextId, Web::MouseEvent const&); bool dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId, Web::MouseEvent const&); bool handle_pinch_event(Web::Compositor::CompositorContextId, Web::PinchEvent const&); - Web::Compositor::AsyncScrollEnqueueResult async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking); - Web::Compositor::AsyncScrollEnqueueResult smooth_scroll_to(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel); + Web::Compositor::AsyncScrollEnqueueResult async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling, Web::Compositor::AsyncScrollOperationTracking); + Web::Compositor::AsyncScrollEnqueueResult smooth_scroll_to(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind); void cancel_smooth_scroll(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID); - bool async_scroll_by(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta); + bool async_scroll_by(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta, Web::Compositor::SnapContainerHandling); Web::Compositor::PendingAsyncScrollUpdates take_pending_async_scroll_updates(Web::Compositor::CompositorContextId); void viewport_size_updated(Web::Compositor::CompositorContextId, Gfx::IntSize, Web::Compositor::WindowResizingInProgress); void set_display_metadata(Web::Compositor::CompositorContextId, Optional display_id, double refresh_rate); diff --git a/Services/Compositor/CompositorWebContentServer.ipc b/Services/Compositor/CompositorWebContentServer.ipc index 5f643d5618187..24e0c535d2e9e 100644 --- a/Services/Compositor/CompositorWebContentServer.ipc +++ b/Services/Compositor/CompositorWebContentServer.ipc @@ -53,8 +53,8 @@ endpoint CompositorWebContentServer webgl_read_buffer_sub_data(Web::Painting::CanvasId canvas_id, u32 target, i64 offset, i64 size, Core::AnonymousBuffer data) => (bool success) invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId context_id, u64 generation) =| - async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking operation_tracking) => (Web::Compositor::AsyncScrollEnqueueResult result) - smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) => (Web::Compositor::AsyncScrollEnqueueResult result) + async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling snap_container_handling, Web::Compositor::AsyncScrollOperationTracking operation_tracking) => (Web::Compositor::AsyncScrollEnqueueResult result) + smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind animation_kind) => (Web::Compositor::AsyncScrollEnqueueResult result) cancel_smooth_scroll(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id) =| take_pending_async_scroll_updates(Web::Compositor::CompositorContextId context_id) => (Web::Compositor::PendingAsyncScrollUpdates updates) diff --git a/Services/Compositor/ConnectionFromClient.cpp b/Services/Compositor/ConnectionFromClient.cpp index 51572e8e7353f..32fd00da1e19e 100644 --- a/Services/Compositor/ConnectionFromClient.cpp +++ b/Services/Compositor/ConnectionFromClient.cpp @@ -94,9 +94,9 @@ Messages::CompositorControlServer::HandlePinchEventResponse ConnectionFromClient return m_compositor_state->handle_pinch_event(context_id, event); } -Messages::CompositorControlServer::AsyncScrollByResponse ConnectionFromClient::async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels) +Messages::CompositorControlServer::AsyncScrollByResponse ConnectionFromClient::async_scroll_by(Web::Compositor::CompositorContextId context_id, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels, Web::Compositor::SnapContainerHandling snap_container_handling) { - return m_compositor_state->async_scroll_by(context_id, position, delta_in_device_pixels); + return m_compositor_state->async_scroll_by(context_id, position, delta_in_device_pixels, snap_container_handling); } void ConnectionFromClient::presented_bitmap_ready_to_paint(Web::Compositor::CompositorContextId context_id, i32 bitmap_id) diff --git a/Services/Compositor/ConnectionFromClient.h b/Services/Compositor/ConnectionFromClient.h index de4696c549890..350f8c5eb5989 100644 --- a/Services/Compositor/ConnectionFromClient.h +++ b/Services/Compositor/ConnectionFromClient.h @@ -43,7 +43,7 @@ class ConnectionFromClient final virtual Messages::CompositorControlServer::HandleMouseEventResponse handle_mouse_event(Web::Compositor::CompositorContextId, Web::MouseEvent) override; virtual Messages::CompositorControlServer::DispatchMouseEventToWebContentResponse dispatch_mouse_event_to_web_content(Web::Compositor::CompositorContextId, Web::MouseEvent) override; virtual Messages::CompositorControlServer::HandlePinchEventResponse handle_pinch_event(Web::Compositor::CompositorContextId, Web::PinchEvent) override; - virtual Messages::CompositorControlServer::AsyncScrollByResponse async_scroll_by(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels) override; + virtual Messages::CompositorControlServer::AsyncScrollByResponse async_scroll_by(Web::Compositor::CompositorContextId, Gfx::FloatPoint position, Gfx::FloatPoint delta_in_device_pixels, Web::Compositor::SnapContainerHandling) override; virtual void presented_bitmap_ready_to_paint(Web::Compositor::CompositorContextId, i32 bitmap_id) override; virtual void set_client_gpu_presentation_capability(bool supported, u64 adapter_luid) override; virtual void crash() override; diff --git a/Services/Compositor/ConnectionFromWebContent.cpp b/Services/Compositor/ConnectionFromWebContent.cpp index 5535559717b38..a893460ab1987 100644 --- a/Services/Compositor/ConnectionFromWebContent.cpp +++ b/Services/Compositor/ConnectionFromWebContent.cpp @@ -261,21 +261,21 @@ void ConnectionFromWebContent::invalidate_wheel_event_listener_state(Web::Compos m_compositor_state->invalidate_wheel_event_listener_state(context_id, generation); } -Messages::CompositorWebContentServer::AsyncScrollByResponse ConnectionFromWebContent::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking operation_tracking) +Messages::CompositorWebContentServer::AsyncScrollByResponse ConnectionFromWebContent::async_scroll_by(Web::Compositor::CompositorContextId context_id, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling snap_container_handling, Web::Compositor::AsyncScrollOperationTracking operation_tracking) { if (!context_is_owned_by_this_connection(context_id)) return Web::Compositor::AsyncScrollEnqueueResult {}; - auto result = m_compositor_state->async_scroll_by(context_id, document_id, position, delta, viewport_rect, operation_tracking); + auto result = m_compositor_state->async_scroll_by(context_id, document_id, position, delta, viewport_rect, snap_container_handling, operation_tracking); if (result.accepted) async_request_rendering_update(); return result; } -Messages::CompositorWebContentServer::SmoothScrollToResponse ConnectionFromWebContent::smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) +Messages::CompositorWebContentServer::SmoothScrollToResponse ConnectionFromWebContent::smooth_scroll_to(Web::Compositor::CompositorContextId context_id, Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind animation_kind) { if (!context_is_owned_by_this_connection(context_id)) return Web::Compositor::AsyncScrollEnqueueResult {}; - auto result = m_compositor_state->smooth_scroll_to(context_id, stable_node_id, offset, viewport_rect, device_pixels_per_css_pixel); + auto result = m_compositor_state->smooth_scroll_to(context_id, stable_node_id, offset, main_thread_offset, viewport_rect, device_pixels_per_css_pixel, animation_kind); if (result.accepted) async_request_rendering_update(); return result; diff --git a/Services/Compositor/ConnectionFromWebContent.h b/Services/Compositor/ConnectionFromWebContent.h index 084b74d6fccd6..127a225a5227b 100644 --- a/Services/Compositor/ConnectionFromWebContent.h +++ b/Services/Compositor/ConnectionFromWebContent.h @@ -64,8 +64,8 @@ class ConnectionFromWebContent final virtual Messages::CompositorWebContentServer::WebglReadPixelsResponse webgl_read_pixels(Web::Painting::CanvasId canvas_id, i32 x, i32 y, i32 width, i32 height, u32 format, u32 type, i32 buf_size, Core::AnonymousBuffer pixels) override; virtual Messages::CompositorWebContentServer::WebglReadBufferSubDataResponse webgl_read_buffer_sub_data(Web::Painting::CanvasId canvas_id, u32 target, i64 offset, i64 size, Core::AnonymousBuffer data) override; virtual void invalidate_wheel_event_listener_state(Web::Compositor::CompositorContextId, u64 generation) override; - virtual Messages::CompositorWebContentServer::AsyncScrollByResponse async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::AsyncScrollOperationTracking) override; - virtual Messages::CompositorWebContentServer::SmoothScrollToResponse smooth_scroll_to(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) override; + virtual Messages::CompositorWebContentServer::AsyncScrollByResponse async_scroll_by(Web::Compositor::CompositorContextId, Web::UniqueNodeID document_id, Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, Web::Compositor::SnapContainerHandling, Web::Compositor::AsyncScrollOperationTracking) override; + virtual Messages::CompositorWebContentServer::SmoothScrollToResponse smooth_scroll_to(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind) override; virtual void cancel_smooth_scroll(Web::Compositor::CompositorContextId, Web::Compositor::AsyncScrollNodeStableID) override; virtual Messages::CompositorWebContentServer::TakePendingAsyncScrollUpdatesResponse take_pending_async_scroll_updates(Web::Compositor::CompositorContextId) override; virtual void viewport_size_updated(Web::Compositor::CompositorContextId, Gfx::IntSize viewport_size, Web::Compositor::WindowResizingInProgress) override; diff --git a/Services/Compositor/ContextState.cpp b/Services/Compositor/ContextState.cpp index b06c85fce01e2..5eda79c07c8aa 100644 --- a/Services/Compositor/ContextState.cpp +++ b/Services/Compositor/ContextState.cpp @@ -174,7 +174,9 @@ void ContextState::install_display_list_update( m_visual_context_tree_for_compositing.clear(); } + auto was_dragging_viewport_scrollbar = m_viewport_scrollbar_controller.has_captured_scrollbar(); m_viewport_scrollbar_controller.set_scrollbars(async_scrolling_state.viewport_scrollbars); + note_user_scroll_gesture_end_if_drag_ended(was_dragging_viewport_scrollbar); m_async_scroll_tree.set_state(move(async_scrolling_state)); if (!m_pending_async_scroll_offsets.is_empty()) { if (auto viewport_scroll_offset = reapply_pending_async_scroll_offsets(m_pending_async_scroll_offsets); viewport_scroll_offset.has_value()) @@ -280,18 +282,22 @@ ContextState::ContextUpdateResult ContextState::handle_mouse_event(Web::MouseEve }; } case Web::MouseEvent::Type::MouseUp: { + auto was_dragging_viewport_scrollbar = m_viewport_scrollbar_controller.has_captured_scrollbar(); auto drag = m_viewport_scrollbar_controller.release_captured_drag(position); if (!drag.has_value()) return {}; + note_user_scroll_gesture_end_if_drag_ended(was_dragging_viewport_scrollbar); + ContextUpdateResult result; result.accepted = true; + // The main thread learns of the release from the next update it takes, so one is asked for even when the + // release scrolls nothing. + result.should_request_rendering_update = true; if (!m_async_scrolling_viewport_rect.is_empty()) result.frame_to_present = m_async_scrolling_viewport_rect; - if (auto frame_to_present = apply_viewport_scrollbar_drag(*drag); frame_to_present.has_value()) { + if (auto frame_to_present = apply_viewport_scrollbar_drag(*drag); frame_to_present.has_value()) result.frame_to_present = *frame_to_present; - result.should_request_rendering_update = true; - } return result; } case Web::MouseEvent::Type::MouseLeave: { @@ -363,18 +369,21 @@ ContextState::AsyncScrollResult ContextState::async_scroll_by( Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, + Web::Compositor::SnapContainerHandling snap_container_handling, Web::Compositor::AsyncScrollOperationTracking operation_tracking) { if (!m_can_accept_async_wheel_events) return {}; - auto scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, delta); + // Scroll node chaining selects a scrolling box the main thread never examined, so the snap containers among the + // chained boxes are recognized here rather than only before the step is admitted to this path. + auto scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, delta, snap_container_handling); if (scroll_target.blocked_by_main_thread_region || scroll_target.blocked_by_wheel_event_region || !scroll_target.node_id.has_value()) return {}; if (scroll_target.node_id->document_id != expected_document_id) return {}; - cancel_smooth_scroll_for_node(*scroll_target.node_id); + cancel_smooth_scroll_taken_over_by_user_input(*scroll_target.node_id); Optional operation_id; if (operation_tracking == Web::Compositor::AsyncScrollOperationTracking::Yes) @@ -399,7 +408,7 @@ ContextState::AsyncScrollResult ContextState::async_scroll_by( return { .enqueue_result = { true, operation_id }, .frame_to_present = async_scroll_viewport_rect }; } -ContextState::AsyncScrollResult ContextState::smooth_scroll_to(Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint destination_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel) +ContextState::AsyncScrollResult ContextState::smooth_scroll_to(Web::Compositor::AsyncScrollNodeStableID stable_node_id, Gfx::FloatPoint destination_offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind animation_kind) { if (!m_has_async_scrolling_state) return {}; @@ -413,8 +422,14 @@ ContextState::AsyncScrollResult ContextState::smooth_scroll_to(Web::Compositor:: cancel_smooth_scroll(stable_node_id); + // The main thread can apply instant input scrolls that this context has not learned about yet, so a scroll whose + // presented offset already matches the destination starts from the main thread's offset to converge on it. + auto start_offset = *current_offset; + if (start_offset == destination_offset) + start_offset = main_thread_offset; + auto operation_id = ++m_next_async_scroll_operation_id; - Web::Compositor::SmoothScrollAnimation animation { *current_offset, destination_offset, device_pixels_per_css_pixel }; + Web::Compositor::SmoothScrollAnimation animation { start_offset, destination_offset, device_pixels_per_css_pixel, animation_kind }; if (animation.duration().is_zero()) { m_completed_async_scroll_operation_ids.append(operation_id); request_rendering_update(); @@ -509,14 +524,14 @@ Optional ContextState::advance_smooth_scroll_animations(MonotonicT return {}; } -ContextState::ContextUpdateResult ContextState::async_scroll_by(Gfx::FloatPoint position, Gfx::FloatPoint delta) +ContextState::ContextUpdateResult ContextState::async_scroll_by(Gfx::FloatPoint position, Gfx::FloatPoint delta, Web::Compositor::SnapContainerHandling snap_container_handling) { if (!presents_to_client()) return {}; if (!m_can_accept_async_wheel_events) return {}; - auto initial_scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, delta); + auto initial_scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, delta, snap_container_handling); if (initial_scroll_target.blocked_by_main_thread_region || initial_scroll_target.blocked_by_wheel_event_region) return {}; @@ -541,7 +556,7 @@ ContextState::ContextUpdateResult ContextState::async_scroll_by(Gfx::FloatPoint if (auto scale = visual_viewport_scale_for_compositing(); scale.has_value() && *scale > 1.0f) async_scroll_delta.scale_by(1.0f / *scale); - auto scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, async_scroll_delta); + auto scroll_target = m_async_scroll_tree.hit_test_scroll_node_for_wheel(position, async_scroll_delta, snap_container_handling); if (scroll_target.blocked_by_main_thread_region || scroll_target.blocked_by_wheel_event_region || !scroll_target.node_id.has_value()) { if (frame_to_present.has_value()) return { @@ -552,7 +567,7 @@ ContextState::ContextUpdateResult ContextState::async_scroll_by(Gfx::FloatPoint return {}; } - cancel_smooth_scroll_for_node(*scroll_target.node_id); + cancel_smooth_scroll_taken_over_by_user_input(*scroll_target.node_id); auto async_scroll_viewport_rect = m_async_scrolling_viewport_rect; auto scroll_offsets = m_async_scroll_tree.apply_scroll_delta(*scroll_target.node_id, async_scroll_delta, m_scroll_state_snapshot); @@ -584,6 +599,10 @@ Web::Compositor::PendingAsyncScrollUpdates ContextState::take_pending_async_scro Web::Compositor::PendingAsyncScrollUpdates updates; AK::swap(updates.scroll_offsets, m_pending_async_scroll_offsets); AK::swap(updates.completed_operation_ids, m_completed_async_scroll_operation_ids); + AK::swap(updates.operation_ids_taken_over_by_user_input, m_async_scroll_operation_ids_taken_over_by_user_input); + updates.user_scroll_gesture_in_progress = m_viewport_scrollbar_controller.has_captured_scrollbar(); + updates.user_scroll_gesture_ended = m_user_scroll_gesture_ended; + m_user_scroll_gesture_ended = false; return updates; } @@ -914,24 +933,31 @@ void ContextState::store_pending_async_scroll_offsets( m_completed_async_scroll_operation_ids.append(*operation_id); } -void ContextState::cancel_smooth_scroll_for_node(Web::Compositor::AsyncScrollNodeID node_id) +void ContextState::cancel_smooth_scroll_taken_over_by_user_input(Web::Compositor::AsyncScrollNodeID node_id) { for (auto const& smooth_scroll_animation : m_smooth_scroll_animations) { auto animated_node_id = m_async_scroll_tree.scroll_node_id_for_stable_id(smooth_scroll_animation.stable_node_id); if (animated_node_id != node_id) continue; + m_async_scroll_operation_ids_taken_over_by_user_input.append(smooth_scroll_animation.operation_id); cancel_smooth_scroll(smooth_scroll_animation.stable_node_id); return; } } +void ContextState::note_user_scroll_gesture_end_if_drag_ended(bool was_dragging_viewport_scrollbar) +{ + if (was_dragging_viewport_scrollbar && !m_viewport_scrollbar_controller.has_captured_scrollbar()) + m_user_scroll_gesture_ended = true; +} + Optional ContextState::apply_viewport_scrollbar_drag(ViewportScrollbarController::Drag const& drag) { auto scroll_delta = m_viewport_scrollbar_controller.scroll_delta_for_drag(m_async_scroll_tree, m_scroll_state_snapshot, drag); if (!scroll_delta.has_value()) return {}; - cancel_smooth_scroll_for_node(scroll_delta->scroll_node_id); + cancel_smooth_scroll_taken_over_by_user_input(scroll_delta->scroll_node_id); auto scroll_offsets = m_async_scroll_tree.apply_scroll_delta(scroll_delta->scroll_node_id, scroll_delta->delta, m_scroll_state_snapshot); if (scroll_offsets.is_empty()) return {}; diff --git a/Services/Compositor/ContextState.h b/Services/Compositor/ContextState.h index 888a00f9f69d7..d1dde9c4fe743 100644 --- a/Services/Compositor/ContextState.h +++ b/Services/Compositor/ContextState.h @@ -115,12 +115,13 @@ class ContextState { Gfx::FloatPoint position, Gfx::FloatPoint delta, Gfx::IntRect viewport_rect, + Web::Compositor::SnapContainerHandling, Web::Compositor::AsyncScrollOperationTracking); - AsyncScrollResult smooth_scroll_to(Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel); + AsyncScrollResult smooth_scroll_to(Web::Compositor::AsyncScrollNodeStableID, Gfx::FloatPoint offset, Gfx::FloatPoint main_thread_offset, Gfx::IntRect viewport_rect, double device_pixels_per_css_pixel, Web::Compositor::ScrollAnimationKind); void cancel_smooth_scroll(Web::Compositor::AsyncScrollNodeStableID); Optional advance_smooth_scroll_animations(MonotonicTime now); bool has_active_smooth_scroll_animations() const { return !m_smooth_scroll_animations.is_empty(); } - ContextUpdateResult async_scroll_by(Gfx::FloatPoint position, Gfx::FloatPoint delta); + ContextUpdateResult async_scroll_by(Gfx::FloatPoint position, Gfx::FloatPoint delta, Web::Compositor::SnapContainerHandling); bool should_defer_main_thread_present_for_async_scroll() const; Web::Compositor::PendingAsyncScrollUpdates take_pending_async_scroll_updates(); @@ -171,7 +172,8 @@ class ContextState { Optional apply_visual_viewport_scroll_delta(Gfx::FloatPoint); Optional reapply_pending_async_scroll_offsets(Vector const&); void store_pending_async_scroll_offsets(Vector const&, Optional = {}); - void cancel_smooth_scroll_for_node(Web::Compositor::AsyncScrollNodeID); + void cancel_smooth_scroll_taken_over_by_user_input(Web::Compositor::AsyncScrollNodeID); + void note_user_scroll_gesture_end_if_drag_ended(bool was_dragging_viewport_scrollbar); Optional apply_viewport_scrollbar_drag(ViewportScrollbarController::Drag const&); void rebuild_wheel_hit_test_targets(); bool is_present_blocked() const; @@ -201,6 +203,8 @@ class ContextState { Vector m_pending_async_scroll_offsets; Vector m_completed_async_scroll_operation_ids; + Vector m_async_scroll_operation_ids_taken_over_by_user_input; + bool m_user_scroll_gesture_ended { false }; Vector m_smooth_scroll_animations; Web::Compositor::AsyncScrollOperationID m_next_async_scroll_operation_id { 0 }; Gfx::IntRect m_async_scrolling_viewport_rect; diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index c2fd38140ba42..807823d897ce1 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -462,7 +462,9 @@ void ConnectionFromClient::mouse_event(u64 page_id, Web::MouseEvent event) if (mouse_event->type != event.type) return nullptr; if (event.type == Web::MouseEvent::Type::MouseWheel - && mouse_event->async_scroll_performed_default_action != event.async_scroll_performed_default_action) + && (mouse_event->async_scroll_performed_default_action != event.async_scroll_performed_default_action + || mouse_event->wheel_delta_precision != event.wheel_delta_precision + || mouse_event->scroll_gesture_phase != event.scroll_gesture_phase)) return nullptr; return mouse_event; } diff --git a/Tests/Compositor/TestContextState.cpp b/Tests/Compositor/TestContextState.cpp index f40414c14d566..282d9975cc47e 100644 --- a/Tests/Compositor/TestContextState.cpp +++ b/Tests/Compositor/TestContextState.cpp @@ -12,7 +12,9 @@ #include #include #include +#include #include +#include struct TestWebContentClient final : public Compositor::CompositorStateWebContentClient { virtual void dispatch_mouse_event_to_web_content(u64, Web::MouseEvent const&) override { } @@ -21,35 +23,37 @@ struct TestWebContentClient final : public Compositor::CompositorStateWebContent virtual void release_video_edge(Media::VideoSinkHandle) override { } }; -static NonnullRefPtr make_display_list(Web::Painting::AccumulatedVisualContextTree const& visual_context_tree, Optional color, Optional surface_clear_color = {}) +template +static void append_display_list_command(ByteBuffer& command_bytes, Command const& command, Optional bounding_rect = {}) { - ByteBuffer command_bytes; - if (color.has_value()) { - auto command = Web::Painting::FillRect { { 0, 0, 4, 4 }, *color }; - auto payload = Web::Painting::display_list_object_bytes(command); - auto record_size = sizeof(Web::Painting::DisplayListCommandHeader) + payload.size(); - auto payload_size = align_up_to(record_size, Web::Painting::DisplayList::command_alignment) - sizeof(Web::Painting::DisplayListCommandHeader); - Web::Painting::DisplayListCommandHeader header { - .command_type = Web::Painting::FillRect::command_type, - .payload_size = static_cast(payload_size), - .context_index = Web::Painting::VISUAL_VIEWPORT_NODE_INDEX, - .context_geometry_only = false, - .has_bounding_rect = true, - .is_clip = false, - .bounding_rect = command.rect, - }; - command_bytes.append(Web::Painting::display_list_object_bytes(header)); - command_bytes.append(payload); - command_bytes.resize(sizeof(header) + payload_size, ByteBuffer::ZeroFillNewElements::Yes); - } + auto payload = Web::Painting::display_list_object_bytes(command); + auto record_size = sizeof(Web::Painting::DisplayListCommandHeader) + payload.size(); + auto payload_size = align_up_to(record_size, Web::Painting::DisplayList::command_alignment) - sizeof(Web::Painting::DisplayListCommandHeader); + Web::Painting::DisplayListCommandHeader header { + .command_type = Command::command_type, + .payload_size = static_cast(payload_size), + .context_index = Web::Painting::VISUAL_VIEWPORT_NODE_INDEX, + .context_geometry_only = false, + .has_bounding_rect = bounding_rect.has_value(), + .is_clip = false, + .bounding_rect = bounding_rect.value_or({}), + }; + + auto size_before_command = command_bytes.size(); + command_bytes.append(Web::Painting::display_list_object_bytes(header)); + command_bytes.append(payload); + command_bytes.resize(size_before_command + sizeof(header) + payload_size, ByteBuffer::ZeroFillNewElements::Yes); +} +static NonnullRefPtr make_display_list_from_commands(Web::Painting::AccumulatedVisualContextTree const& visual_context_tree, ByteBuffer command_bytes, Optional surface_clear_color, Optional async_scrolling_metadata) +{ IPC::MessageBuffer buffer; IPC::Encoder encoder { buffer }; MUST(encoder.encode(static_cast(1))); MUST(encoder.encode(command_bytes)); MUST(encoder.encode(visual_context_tree.version())); MUST(encoder.encode(surface_clear_color)); - MUST(encoder.encode(Optional {})); + MUST(encoder.encode(async_scrolling_metadata)); MUST(encoder.encode(HashMap {})); FixedMemoryStream stream { buffer.data().span() }; @@ -58,6 +62,132 @@ static NonnullRefPtr make_display_list(Web::Painting return MUST(decoder.decode>()); } +static NonnullRefPtr make_display_list(Web::Painting::AccumulatedVisualContextTree const& visual_context_tree, Optional color, Optional surface_clear_color = {}) +{ + ByteBuffer command_bytes; + if (color.has_value()) { + Web::Painting::FillRect command { { 0, 0, 4, 4 }, *color }; + append_display_list_command(command_bytes, command, command.rect); + } + + return make_display_list_from_commands(visual_context_tree, move(command_bytes), surface_clear_color, {}); +} + +static constexpr Web::UniqueNodeID TEST_DOCUMENT_ID { 1 }; +static constexpr Web::Painting::VisualContextIndex TEST_SCROLL_NODE_INDEX { 1 }; +static Gfx::IntRect const TEST_VIEWPORT_RECT { 0, 0, 100, 200 }; +static Gfx::IntRect const TEST_SCROLLBAR_GUTTER_RECT { 90, 0, 10, 200 }; +static Gfx::IntRect const TEST_SCROLLBAR_THUMB_RECT { 90, 0, 10, 50 }; +static constexpr float TEST_MAX_SCROLL_OFFSET = 600; + +// The length of thumb travel per scrolled pixel. +static double const TEST_SCROLLBAR_SCROLL_SIZE = (TEST_SCROLLBAR_GUTTER_RECT.height() - TEST_SCROLLBAR_THUMB_RECT.height()) / TEST_MAX_SCROLL_OFFSET; + +static NonnullRefPtr make_scrollable_viewport_display_list(Web::Painting::AccumulatedVisualContextTree const& visual_context_tree, bool with_viewport_scrollbar) +{ + ByteBuffer command_bytes; + append_display_list_command(command_bytes, Web::Painting::CompositorScrollNode { + .document_id = TEST_DOCUMENT_ID, + .scrollable_node_id = TEST_DOCUMENT_ID, + .scroll_node_index = TEST_SCROLL_NODE_INDEX, + .parent_scroll_node_index = Web::Painting::VISUAL_VIEWPORT_NODE_INDEX, + .scrollport_rect = TEST_VIEWPORT_RECT, + .min_scroll_offset = { 0, 0 }, + .max_scroll_offset = { 0, TEST_MAX_SCROLL_OFFSET }, + .scroll_node_kind = Web::Painting::CompositorScrollNodeKind::Viewport, + .pseudo_element_type = 0, + .is_viewport = true, + .can_be_wheel_scrolled_horizontally = false, + .can_be_wheel_scrolled_vertically = true, + .snaps_scroll_position_horizontally = false, + .snaps_scroll_position_vertically = false, + }); + + if (with_viewport_scrollbar) { + append_display_list_command(command_bytes, Web::Painting::CompositorViewportScrollbar { + .document_id = TEST_DOCUMENT_ID, + .scroll_node_index = TEST_SCROLL_NODE_INDEX, + .gutter_rect = TEST_SCROLLBAR_GUTTER_RECT, + .thumb_rect = TEST_SCROLLBAR_THUMB_RECT, + .expanded_gutter_rect = TEST_SCROLLBAR_GUTTER_RECT, + .expanded_thumb_rect = TEST_SCROLLBAR_THUMB_RECT, + .scroll_size = TEST_SCROLLBAR_SCROLL_SIZE, + .expanded_scroll_size = TEST_SCROLLBAR_SCROLL_SIZE, + .min_scroll_offset = 0, + .max_scroll_offset = TEST_MAX_SCROLL_OFFSET, + .thumb_color = Gfx::Color::Black, + .track_color = Gfx::Color::White, + .vertical = true, + }); + } + + return make_display_list_from_commands(visual_context_tree, move(command_bytes), {}, Web::Painting::DisplayList::AsyncScrollingMetadata { .viewport_rect = TEST_VIEWPORT_RECT }); +} + +static Web::MouseEvent make_mouse_event(Web::MouseEvent::Type type, Web::DevicePixels y) +{ + Web::MouseEvent event; + event.type = type; + event.position = { TEST_SCROLLBAR_THUMB_RECT.x() + TEST_SCROLLBAR_THUMB_RECT.width() / 2, y }; + event.button = Web::UIEvents::MouseButton::Primary; + return event; +} + +TEST_CASE(dragging_a_viewport_scrollbar_reports_a_user_scroll_gesture_until_it_is_released) +{ + TestWebContentClient client; + Web::Painting::CanvasSurfaceRegistry canvas_surface_registry; + Compositor::ContextState context { 0, client, canvas_surface_registry, true }; + auto visual_context_tree = Web::Painting::AccumulatedVisualContextTree::create(); + + context.viewport_size_updated(TEST_VIEWPORT_RECT.size(), Web::Compositor::WindowResizingInProgress::No); + context.install_display_list_update(make_scrollable_viewport_display_list(visual_context_tree, true), visual_context_tree, {}); + + EXPECT(context.handle_mouse_event(make_mouse_event(Web::MouseEvent::Type::MouseDown, TEST_SCROLLBAR_THUMB_RECT.center().y())).accepted); + auto updates = context.take_pending_async_scroll_updates(); + EXPECT(updates.user_scroll_gesture_in_progress); + EXPECT(!updates.user_scroll_gesture_ended); + + EXPECT(context.handle_mouse_event(make_mouse_event(Web::MouseEvent::Type::MouseMove, TEST_SCROLLBAR_THUMB_RECT.center().y() + 100)).accepted); + updates = context.take_pending_async_scroll_updates(); + EXPECT(!updates.scroll_offsets.is_empty()); + EXPECT(updates.user_scroll_gesture_in_progress); + EXPECT(!updates.user_scroll_gesture_ended); + + // The release always asks for a rendering update, so that the main thread learns of it even when nothing scrolled. + auto release_result = context.handle_mouse_event(make_mouse_event(Web::MouseEvent::Type::MouseUp, TEST_SCROLLBAR_THUMB_RECT.center().y() + 100)); + EXPECT(release_result.accepted); + EXPECT(release_result.should_request_rendering_update); + updates = context.take_pending_async_scroll_updates(); + EXPECT(!updates.user_scroll_gesture_in_progress); + EXPECT(updates.user_scroll_gesture_ended); + + // The release is reported once. + updates = context.take_pending_async_scroll_updates(); + EXPECT(!updates.user_scroll_gesture_in_progress); + EXPECT(!updates.user_scroll_gesture_ended); +} + +TEST_CASE(losing_the_scrollbar_a_drag_holds_ends_its_user_scroll_gesture) +{ + TestWebContentClient client; + Web::Painting::CanvasSurfaceRegistry canvas_surface_registry; + Compositor::ContextState context { 0, client, canvas_surface_registry, true }; + auto visual_context_tree = Web::Painting::AccumulatedVisualContextTree::create(); + + context.viewport_size_updated(TEST_VIEWPORT_RECT.size(), Web::Compositor::WindowResizingInProgress::No); + context.install_display_list_update(make_scrollable_viewport_display_list(visual_context_tree, true), visual_context_tree, {}); + + EXPECT(context.handle_mouse_event(make_mouse_event(Web::MouseEvent::Type::MouseDown, TEST_SCROLLBAR_THUMB_RECT.center().y())).accepted); + EXPECT(context.take_pending_async_scroll_updates().user_scroll_gesture_in_progress); + + // The drag cannot outlive the scrollbar it holds. + context.install_display_list_update(make_scrollable_viewport_display_list(visual_context_tree, false), visual_context_tree, {}); + auto updates = context.take_pending_async_scroll_updates(); + EXPECT(!updates.user_scroll_gesture_in_progress); + EXPECT(updates.user_scroll_gesture_ended); +} + TEST_CASE(rasterization_clears_damaged_pixels_to_the_canvas_color_in_presentation_backing_stores) { TestWebContentClient client; diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000-ref.html new file mode 100644 index 0000000000000..a2281dc47c694 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000-ref.html @@ -0,0 +1,89 @@ + + + CSS Scroll Snap Reference + + + +

Test passes if there is an orange square precisely at the top left corner of each blue box (no gap), +and each orange box is empty. + + +

+
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-scroll-snap/snap-after-initial-layout/snap-after-initial-layout-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-scroll-snap/snap-after-initial-layout/snap-after-initial-layout-ref.html new file mode 100644 index 0000000000000..c8009b626cb63 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-scroll-snap/snap-after-initial-layout/snap-after-initial-layout-ref.html @@ -0,0 +1,20 @@ + +Reference + + +
+
+
diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000.html new file mode 100644 index 0000000000000..682e93a0bce6f --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000.html @@ -0,0 +1,121 @@ + + + On-screen vs. Off-screen Snapped Initial Scroll Position (Mandatory and Proximity) + + + + + + + + + +

Test passes if there is an orange square precisely at the top left corner of each blue box (no gap), +and each orange box is empty. + + +

+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-horizontal-tb.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-horizontal-tb.html new file mode 100644 index 0000000000000..b822919e3016c --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-horizontal-tb.html @@ -0,0 +1,52 @@ + + + Scrollers should snap to the closest snap point on initial layout + (using 'writing-mode: horizontal-tb') + + + + + +
+
+
+
+
diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-lr.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-lr.html new file mode 100644 index 0000000000000..1d44cca855ff8 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-lr.html @@ -0,0 +1,52 @@ + + + Scrollers should snap to the closest snap point on initial layout + (using 'writing-mode: vertical-lr') + + + + + +
+
+
+
+
diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-rl.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-rl.html new file mode 100644 index 0000000000000..aa2c81a8f0e54 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-rl.html @@ -0,0 +1,55 @@ + + + Scrollers should snap to the closest snap point on initial layout + (using 'writing-mode: vertical-rl') + + + + + +
+
+
+
+
diff --git a/Tests/LibWeb/TestSmoothScrollAnimation.cpp b/Tests/LibWeb/TestSmoothScrollAnimation.cpp index a7608a57e651c..69e7b38e130e5 100644 --- a/Tests/LibWeb/TestSmoothScrollAnimation.cpp +++ b/Tests/LibWeb/TestSmoothScrollAnimation.cpp @@ -7,6 +7,7 @@ #include #include +using Web::Compositor::ScrollAnimationKind; using Web::Compositor::SmoothScrollAnimation; TEST_CASE(zero_distance_completes_immediately) @@ -53,3 +54,43 @@ TEST_CASE(duration_is_independent_of_device_scale) EXPECT_EQ(css_pixel_animation.duration(), device_pixel_animation.duration()); } + +TEST_CASE(momentum_travels_for_as_long_as_its_distance_takes) +{ + // Momentum covers a longer distance over more of its decaying frames rather than at a greater speed. + SmoothScrollAnimation short_animation({ 0, 0 }, { 0, 200 }, 1.0, ScrollAnimationKind::Momentum); + SmoothScrollAnimation long_animation({ 0, 0 }, { 0, 1000 }, 1.0, ScrollAnimationKind::Momentum); + + EXPECT(short_animation.duration() > AK::Duration::from_milliseconds(200)); + EXPECT(long_animation.duration() > short_animation.duration()); + + // However fast the momentum was, the scroll that replaces it still comes to rest. + SmoothScrollAnimation enormous_animation({ 0, 0 }, { 0, 100'000'000 }, 1.0, ScrollAnimationKind::Momentum); + EXPECT(enormous_animation.duration() <= AK::Duration::from_seconds(5)); +} + +TEST_CASE(momentum_decays_towards_its_destination) +{ + SmoothScrollAnimation animation({ 0, 0 }, { 0, 600 }, 1.0, ScrollAnimationKind::Momentum); + + auto start = animation.sample(AK::Duration::zero()); + EXPECT(!start.complete); + EXPECT_EQ(start.offset, Gfx::FloatPoint(0, 0)); + + // The distance covered by each frame shrinks, so more than half the way is covered in the first half of the time. + auto midpoint = animation.sample(AK::Duration::from_milliseconds(animation.duration().to_milliseconds() / 2)); + EXPECT(!midpoint.complete); + EXPECT(midpoint.offset.y() > 300); + + // The scroll never travels backwards on its way to its destination. + auto previous_offset = 0.0f; + for (auto elapsed = AK::Duration::zero(); elapsed < animation.duration(); elapsed = elapsed + AK::Duration::from_milliseconds(16)) { + auto offset = animation.sample(elapsed).offset.y(); + EXPECT(offset >= previous_offset); + previous_offset = offset; + } + + auto end = animation.sample(animation.duration()); + EXPECT(end.complete); + EXPECT_EQ(end.offset, Gfx::FloatPoint(0, 600)); +} diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index 9066e9c550f82..c289e2fff6943 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -600,6 +600,7 @@ printElement println promiseTest removeTestErrorHandler +scrollendEvent spoofCurrentURL test timeout diff --git a/Tests/LibWeb/Text/expected/async-scrolling/snap-container-wheel-nested-navigable.txt b/Tests/LibWeb/Text/expected/async-scrolling/snap-container-wheel-nested-navigable.txt new file mode 100644 index 0000000000000..26f66efaaf8e5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/async-scrolling/snap-container-wheel-nested-navigable.txt @@ -0,0 +1,2 @@ +nested navigable scrolled: true +snap container scrollY: 0 diff --git a/Tests/LibWeb/Text/expected/async-scrolling/snap-container-wheel-target.txt b/Tests/LibWeb/Text/expected/async-scrolling/snap-container-wheel-target.txt new file mode 100644 index 0000000000000..56b1858578737 --- /dev/null +++ b/Tests/LibWeb/Text/expected/async-scrolling/snap-container-wheel-target.txt @@ -0,0 +1,7 @@ +discrete step along the snapping axis: main-thread +precise delta along the snapping axis: non-viewport +discrete step across the snapping axis: non-viewport +momentum delta along the snapping axis: main-thread +momentum delta across the snapping axis: non-viewport +discrete step chained to the snap container: main-thread +precise delta chained to the snap container: non-viewport diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt index ec8328b2c369e..b3cfd6c47f6d6 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt @@ -293,6 +293,9 @@ All properties associated with getComputedStyle(document.body): "scroll-padding-left", "scroll-padding-right", "scroll-padding-top", + "scroll-snap-align", + "scroll-snap-stop", + "scroll-snap-type", "scroll-timeline-axis", "scroll-timeline-name", "scrollbar-gutter", diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt b/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt index 287987f7c3f45..29f9c89cd77cb 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleProperties-all-supported-properties-and-default-values.txt @@ -806,6 +806,12 @@ All supported properties and their default values exposed from CSSStylePropertie 'scroll-padding-right': 'auto' 'scrollPaddingTop': 'auto' 'scroll-padding-top': 'auto' +'scrollSnapAlign': 'none' +'scroll-snap-align': 'none' +'scrollSnapStop': 'normal' +'scroll-snap-stop': 'normal' +'scrollSnapType': 'none' +'scroll-snap-type': 'none' 'scrollTimeline': 'none' 'scroll-timeline': 'none' 'scrollTimelineAxis': 'block' diff --git a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt index 93573017715d2..2d35ecc1cfb74 100644 --- a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt +++ b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt @@ -291,6 +291,9 @@ scroll-padding-inline-start: auto scroll-padding-left: auto scroll-padding-right: auto scroll-padding-top: auto +scroll-snap-align: none +scroll-snap-stop: normal +scroll-snap-type: none scroll-timeline-axis: block scroll-timeline-name: none scrollbar-gutter: auto @@ -311,7 +314,7 @@ top: auto touch-action: auto transform: none transform-box: view-box -transform-origin: 392px 2034.5px +transform-origin: 392px 2054px transform-style: flat transition-behavior: normal transition-delay: 0s diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-auto-scroll-modes.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-auto-scroll-modes.txt new file mode 100644 index 0000000000000..efc96a83ce6a4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-auto-scroll-modes.txt @@ -0,0 +1,8 @@ +scroll offset changed while the mode was active: false +scrollend count while the mode was active: 0 +scroll offset after the mode was exited: 0,400 +scrollend count after the mode was exited: 1 +scroll offset changed while the drag continued: false +scrollend count while the drag continued: 0 +scroll offset after the drag ended: 0,400 +scrollend count after the drag ended: 1 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-flick.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-flick.txt new file mode 100644 index 0000000000000..cdc41e29c44e9 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-flick.txt @@ -0,0 +1,6 @@ +offset after the flick settled: 0,600 +flick traveled beyond the offset it settled at: false +scrollend events fired by the flick: 1 +offset after the flick back settled: 0,600 +flick back traveled beyond the offset it settled at: false +offset after the flick that never decayed settled: 0,200 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-gesture-settles-during-layout.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-gesture-settles-during-layout.txt new file mode 100644 index 0000000000000..e0ed84b3bf800 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-gesture-settles-during-layout.txt @@ -0,0 +1,2 @@ +the drag ended between snap positions: true +the gesture snapped once layout was up to date: true diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-gesture-whose-end-is-never-reported.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-gesture-whose-end-is-never-reported.txt new file mode 100644 index 0000000000000..e932ef2e69dae --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-gesture-whose-end-is-never-reported.txt @@ -0,0 +1,4 @@ +scroll offset after pausing within the gesture: 0,120 +scrollend events while the gesture is in progress: 0 +scrollend fired after a wheel that reports no phase: true +the scrolling came to rest at a snap area: true diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-homing-keys.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-homing-keys.txt new file mode 100644 index 0000000000000..06c63db308b5e --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-homing-keys.txt @@ -0,0 +1,2 @@ +scroll offset after an end key press: 0,1000 +scroll offset after a home key press: 0,0 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-keyboard-scroll-of-smooth-viewport.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-keyboard-scroll-of-smooth-viewport.txt new file mode 100644 index 0000000000000..dd1fe9144c3da --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-keyboard-scroll-of-smooth-viewport.txt @@ -0,0 +1,3 @@ +down arrow key press scrolls to the second section: PASS +another press scrolls to the third section: PASS +up arrow key press scrolls back to the second section: PASS diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-paging-keys.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-paging-keys.txt new file mode 100644 index 0000000000000..0b9827f4db208 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-paging-keys.txt @@ -0,0 +1,5 @@ +page down snaps to the position nearest one page ahead: PASS +page up snaps to the position nearest one page back: PASS +page down does not snap back to the starting snap position: PASS +shift+space snaps to the position nearest one page back: PASS +space snaps to the position nearest one page ahead: PASS diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-pan-adopted-following-key-scroll.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-pan-adopted-following-key-scroll.txt new file mode 100644 index 0000000000000..1cd504837dcf7 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-pan-adopted-following-key-scroll.txt @@ -0,0 +1 @@ +scroll offset after the pan settles: 0,0 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-repeated-keyboard-scroll.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-repeated-keyboard-scroll.txt new file mode 100644 index 0000000000000..4cefe51311c90 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-repeated-keyboard-scroll.txt @@ -0,0 +1,4 @@ +scroll offset after six down arrow key presses: 0,0 +scroll offset once the presses settle: 0,660 +scroll offset while the up arrow key repeats: 0,660 +scroll offset once the held key is released: 0,0 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-after-scrollbar-drag.txt b/Tests/LibWeb/Text/expected/scroll-snap-after-scrollbar-drag.txt new file mode 100644 index 0000000000000..e56c8413aed17 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-after-scrollbar-drag.txt @@ -0,0 +1,4 @@ +scroll offset after a down arrow key press: 0,500 +scroll offset after dragging the scrollbar to the end: 0,1500 +scroll offset after the momentum gesture ends: 0,0 +scroll offset after dragging the scrollbar to the end again: 0,1500 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-align-writing-mode.txt b/Tests/LibWeb/Text/expected/scroll-snap-align-writing-mode.txt new file mode 100644 index 0000000000000..943e1eb698473 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-align-writing-mode.txt @@ -0,0 +1,3 @@ +scroll offset with horizontal-tb and ltr: 900,0 +scroll offset with horizontal-tb and rtl: -900,0 +scroll offset with vertical-rl and ltr: -900,0 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-at-reported-gesture-end.txt b/Tests/LibWeb/Text/expected/scroll-snap-at-reported-gesture-end.txt new file mode 100644 index 0000000000000..3a12460999d10 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-at-reported-gesture-end.txt @@ -0,0 +1,3 @@ +scroll offset while the gesture is in progress: 0,120 +snap scroll started before the settle deadline: true +scroll offset after the gesture ends: 0,200 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-consecutive-wheel-steps.txt b/Tests/LibWeb/Text/expected/scroll-snap-consecutive-wheel-steps.txt new file mode 100644 index 0000000000000..d62f0dfe334a8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-consecutive-wheel-steps.txt @@ -0,0 +1,3 @@ +scroll offset after two small steps: 0,260 +scroll offset after two large steps: 0,660 +scroll offset after two steps of one snap position: 0,400 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-directional-wheel-scroll.txt b/Tests/LibWeb/Text/expected/scroll-snap-directional-wheel-scroll.txt new file mode 100644 index 0000000000000..61d960bfbc06d --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-directional-wheel-scroll.txt @@ -0,0 +1,5 @@ +scroll offset after small downward step: 0,260 +scrollend events fired by the step: 1 +scroll offset after small upward step: 0,0 +scroll offset at last snap position: 0,460 +scroll offset after step beyond last snap position: 0,460 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-in-pseudo-element-scroll-container.txt b/Tests/LibWeb/Text/expected/scroll-snap-in-pseudo-element-scroll-container.txt new file mode 100644 index 0000000000000..f2af3cb25eab1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-in-pseudo-element-scroll-container.txt @@ -0,0 +1,2 @@ +scroll offset before scrolling the pseudo-element: 0,0 +scroll offset after scrolling the pseudo-element: 0,0 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-new-gesture-during-compositor-snap-scroll.txt b/Tests/LibWeb/Text/expected/scroll-snap-new-gesture-during-compositor-snap-scroll.txt new file mode 100644 index 0000000000000..65858ec7a57b0 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-new-gesture-during-compositor-snap-scroll.txt @@ -0,0 +1,4 @@ +snap scroll in flight: true +scrollend events while the gesture was in progress: 0 +scroll offset after the gesture settled: 0,200 +scrollend events in total: 1 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-new-gesture-during-snap-scroll.txt b/Tests/LibWeb/Text/expected/scroll-snap-new-gesture-during-snap-scroll.txt new file mode 100644 index 0000000000000..65858ec7a57b0 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-new-gesture-during-snap-scroll.txt @@ -0,0 +1,4 @@ +snap scroll in flight: true +scrollend events while the gesture was in progress: 0 +scroll offset after the gesture settled: 0,200 +scrollend events in total: 1 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-resnap-inside-reused-subtree.txt b/Tests/LibWeb/Text/expected/scroll-snap-resnap-inside-reused-subtree.txt new file mode 100644 index 0000000000000..50a038c36a4f9 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-resnap-inside-reused-subtree.txt @@ -0,0 +1,3 @@ +scroll offset after initial snap: 100 +scroll offset after translating the scroller: 100 +scroll offset after moving the snap areas: 150 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-scroll-step-while-pinch-zoomed.txt b/Tests/LibWeb/Text/expected/scroll-snap-scroll-step-while-pinch-zoomed.txt new file mode 100644 index 0000000000000..23503484d1b14 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-scroll-step-while-pinch-zoomed.txt @@ -0,0 +1,5 @@ +scale: 2 +visual viewport panned by the wheel step: true +page scroll offset after the wheel step: 0 +visual viewport panned by the key press: true +page scroll offset after the key press: 0 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-settlement-during-programmatic-scroll.txt b/Tests/LibWeb/Text/expected/scroll-snap-settlement-during-programmatic-scroll.txt new file mode 100644 index 0000000000000..156effbdd552e --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-settlement-during-programmatic-scroll.txt @@ -0,0 +1,3 @@ +scroll offset after the pan: 0,60 +scroll offset after the programmatic scroll: 0,1000 +scroll offset once everything settles: 0,1000 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-spaced-wheel-steps.txt b/Tests/LibWeb/Text/expected/scroll-snap-spaced-wheel-steps.txt new file mode 100644 index 0000000000000..186ac72751be8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-spaced-wheel-steps.txt @@ -0,0 +1,4 @@ +scroll offset after the first step: 0,400 +scroll offset after a second step of the same gesture: 0,400 +scroll offset after a step taking them past the next snap position: 0,800 +scroll offset after the first step of a new gesture: 0,1200 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-stop-always-during-pan.txt b/Tests/LibWeb/Text/expected/scroll-snap-stop-always-during-pan.txt new file mode 100644 index 0000000000000..77afb03fa1f4d --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-stop-always-during-pan.txt @@ -0,0 +1,3 @@ +scroll offset after dragging over three snap positions: 0,600 +scroll offset after flicking over three snap positions: 0,200 +scroll offset after flicking back over three snap positions: 0,600 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-stop-wheel-step.txt b/Tests/LibWeb/Text/expected/scroll-snap-stop-wheel-step.txt new file mode 100644 index 0000000000000..0bba0e12d0b0d --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-stop-wheel-step.txt @@ -0,0 +1 @@ +scroll offset after a step that reaches beyond the stop: 0,400 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-chained-to-snap-container.txt b/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-chained-to-snap-container.txt new file mode 100644 index 0000000000000..92d3b112f9777 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-chained-to-snap-container.txt @@ -0,0 +1,2 @@ +inner scroll offset: 0,100 +snap container scroll offset: 0,200 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-during-programmatic-scroll.txt b/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-during-programmatic-scroll.txt new file mode 100644 index 0000000000000..0a5ef1c702156 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-during-programmatic-scroll.txt @@ -0,0 +1,3 @@ +scroll offset after the first step: 0,100 +step travels on from the offset it arrived at: true +step lands on a snap position: true diff --git a/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-in-one-axis.txt b/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-in-one-axis.txt new file mode 100644 index 0000000000000..020b12e10b7d7 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-in-one-axis.txt @@ -0,0 +1 @@ +scroll offset after a vertical step: 0,200 diff --git a/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-past-nested-navigable-extent.txt b/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-past-nested-navigable-extent.txt new file mode 100644 index 0000000000000..e399b7c9609a9 --- /dev/null +++ b/Tests/LibWeb/Text/expected/scroll-snap-wheel-step-past-nested-navigable-extent.txt @@ -0,0 +1,4 @@ +nested navigable scroll offset after the first step: 200 +page scroll offset after the first step: 0 +nested navigable scroll offset after the second step: 200 +page scroll offset after the second step: 200 diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt index 80b76bcdeada1..46f1a4db63dfa 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt @@ -1,8 +1,8 @@ Harness status: OK -Found 309 tests +Found 312 tests -305 Pass +308 Pass 4 Fail Pass accent-color Pass border-collapse @@ -271,6 +271,9 @@ Pass scroll-padding-inline-start Pass scroll-padding-left Pass scroll-padding-right Pass scroll-padding-top +Pass scroll-snap-align +Pass scroll-snap-stop +Pass scroll-snap-type Pass scroll-timeline-axis Pass scroll-timeline-name Pass scrollbar-gutter diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.txt new file mode 100644 index 0000000000000..2b05ea6f5b590 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Ignore snap points orthogonal to scroll snap axis \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/inheritance.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/inheritance.txt new file mode 100644 index 0000000000000..d4ae55885b12b --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/inheritance.txt @@ -0,0 +1,43 @@ +Harness status: OK + +Found 38 tests + +38 Pass +Pass Property scroll-margin-block-end has initial value 0px +Pass Property scroll-margin-block-end does not inherit +Pass Property scroll-margin-block-start has initial value 0px +Pass Property scroll-margin-block-start does not inherit +Pass Property scroll-margin-bottom has initial value 0px +Pass Property scroll-margin-bottom does not inherit +Pass Property scroll-margin-inline-end has initial value 0px +Pass Property scroll-margin-inline-end does not inherit +Pass Property scroll-margin-inline-start has initial value 0px +Pass Property scroll-margin-inline-start does not inherit +Pass Property scroll-margin-left has initial value 0px +Pass Property scroll-margin-left does not inherit +Pass Property scroll-margin-right has initial value 0px +Pass Property scroll-margin-right does not inherit +Pass Property scroll-margin-top has initial value 0px +Pass Property scroll-margin-top does not inherit +Pass Property scroll-padding-block-end has initial value auto +Pass Property scroll-padding-block-end does not inherit +Pass Property scroll-padding-block-start has initial value auto +Pass Property scroll-padding-block-start does not inherit +Pass Property scroll-padding-bottom has initial value auto +Pass Property scroll-padding-bottom does not inherit +Pass Property scroll-padding-inline-end has initial value auto +Pass Property scroll-padding-inline-end does not inherit +Pass Property scroll-padding-inline-start has initial value auto +Pass Property scroll-padding-inline-start does not inherit +Pass Property scroll-padding-left has initial value auto +Pass Property scroll-padding-left does not inherit +Pass Property scroll-padding-right has initial value auto +Pass Property scroll-padding-right does not inherit +Pass Property scroll-padding-top has initial value auto +Pass Property scroll-padding-top does not inherit +Pass Property scroll-snap-align has initial value none +Pass Property scroll-snap-align does not inherit +Pass Property scroll-snap-stop has initial value normal +Pass Property scroll-snap-stop does not inherit +Pass Property scroll-snap-type has initial value none +Pass Property scroll-snap-type does not inherit \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/keyboard.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/keyboard.txt new file mode 100644 index 0000000000000..140b7041db7ee --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/keyboard.txt @@ -0,0 +1,13 @@ +Harness status: OK + +Found 8 tests + +8 Pass +Pass Snaps to bottom-left after pressing ArrowDown +Pass Snaps to top-left after pressing ArrowUp +Pass Snaps to top-right after pressing ArrowRight +Pass Snaps to top-left after pressing ArrowLeft +Pass If the original intended offset is valid as making a snap area cover thesnapport, and there's no other snap offset in between, use the originalintended offset +Pass If the original intended offset is valid as making a snap area cover the snapport, but there's a defined snap offset in between, use the defined snap offset. +Pass If there is no valid snap offset on the arrow key's direction other than the current offset, and the scroll-snap-type is mandatory, stay at the current offset. +Pass If there is no valid snap offset on the arrow key's direction other than the current offset, and the scroll-snap-type is proximity, go to the original intended offset \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/mouse-wheel.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/mouse-wheel.txt new file mode 100644 index 0000000000000..fe0d263423d2b --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/mouse-wheel.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Wheel-scroll triggers snap to target position without intermediate pause. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.txt new file mode 100644 index 0000000000000..e078021aba5aa --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass Keyboard scrolling with vertical snap-area overflow +Pass Mouse-wheel scrolling with vertical snap-area overflow \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/no-snap-position.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/no-snap-position.txt new file mode 100644 index 0000000000000..cbab00777e89e --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/no-snap-position.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 3 tests + +3 Pass +Pass No snapping occurs if there is no valid snap position +Pass No snapping occurs if there is no valid snap position matches scroll-snap-type +Pass No snapping occurs when last remaining valid snap point is no longer valid. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/overflowing-snap-areas.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/overflowing-snap-areas.txt new file mode 100644 index 0000000000000..3cd4cb9d72658 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/overflowing-snap-areas.txt @@ -0,0 +1,16 @@ +Harness status: OK + +Found 11 tests + +11 Pass +Pass Snaps to the snap position if the snap area doesn't cover the snapport on x. +Pass Snaps to the snap position if the snap area covers the snapport on x on the right border. +Pass Snaps to the snap position if the snap area covers the snapport on x on the left border. +Pass Snaps to a snap area (400) that is closer than the position that reveals the space between snap areas (600) within the larger snap area on x. +Pass Snaps to a snap area (400) that is closer than the position that reveals the space between snap areas (600) within the larger snap area on y. +Pass Snap to current scroll position which is a valid snap position, as the snap area covers snapport on x and there is no intruding snap area. +Pass Snap to current scroll position which is a valid snap position, as the snap area covers snapport on y and there is no intruding snap area. +Pass Don't snap back even if scrollTo tries to scroll to positions which are outside of the scroll range and if a snap target element covers the snaport +Pass Snap to current scroll position on x as the area is covering x axis.However, we snap to the specified snap position on y as the area is not covering y axis. +Pass snap to current scroll position on y as the area is covering y axis, even though that area is not the only scroll area at the same position. +Pass snap to current scroll position on x as the area is covering x axis, even though that area is not the only scroll area at the same position. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/overscroll-snap.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/overscroll-snap.txt new file mode 100644 index 0000000000000..eb9d9dc4d4ef8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/overscroll-snap.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass snapport covered by snap area doesn't jump \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-computed.txt new file mode 100644 index 0000000000000..96d86e4423170 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-computed.txt @@ -0,0 +1,12 @@ +Harness status: OK + +Found 7 tests + +7 Pass +Pass Property scroll-snap-align value 'none' +Pass Property scroll-snap-align value 'start' +Pass Property scroll-snap-align value 'end' +Pass Property scroll-snap-align value 'center' +Pass Property scroll-snap-align value 'start none' +Pass Property scroll-snap-align value 'center end' +Pass Property scroll-snap-align value 'start start' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-invalid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-invalid.txt new file mode 100644 index 0000000000000..ecf5ce9b53831 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-invalid.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 3 tests + +3 Pass +Pass e.style['scroll-snap-align'] = "auto" should not set the property value +Pass e.style['scroll-snap-align'] = "start invalid" should not set the property value +Pass e.style['scroll-snap-align'] = "start end center" should not set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-valid.txt new file mode 100644 index 0000000000000..fe44b88002764 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-valid.txt @@ -0,0 +1,12 @@ +Harness status: OK + +Found 7 tests + +7 Pass +Pass e.style['scroll-snap-align'] = "none" should set the property value +Pass e.style['scroll-snap-align'] = "start" should set the property value +Pass e.style['scroll-snap-align'] = "end" should set the property value +Pass e.style['scroll-snap-align'] = "center" should set the property value +Pass e.style['scroll-snap-align'] = "start none" should set the property value +Pass e.style['scroll-snap-align'] = "center end" should set the property value +Pass e.style['scroll-snap-align'] = "start start" should set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-computed.txt new file mode 100644 index 0000000000000..9bfc107e25752 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-computed.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass Property scroll-snap-stop value 'normal' +Pass Property scroll-snap-stop value 'always' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.txt new file mode 100644 index 0000000000000..55c3491afe0d6 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass e.style['scroll-snap-stop'] = "auto" should not set the property value +Pass e.style['scroll-snap-stop'] = "normal always" should not set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-valid.txt new file mode 100644 index 0000000000000..420f3fc726091 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-valid.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass e.style['scroll-snap-stop'] = "normal" should set the property value +Pass e.style['scroll-snap-stop'] = "always" should set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-computed.txt new file mode 100644 index 0000000000000..34f876f11e06c --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-computed.txt @@ -0,0 +1,13 @@ +Harness status: OK + +Found 8 tests + +8 Pass +Pass Property scroll-snap-type value 'none' +Pass Property scroll-snap-type value 'x' +Pass Property scroll-snap-type value 'y' +Pass Property scroll-snap-type value 'block' +Pass Property scroll-snap-type value 'inline' +Pass Property scroll-snap-type value 'both' +Pass Property scroll-snap-type value 'y mandatory' +Pass Property scroll-snap-type value 'inline proximity' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-invalid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-invalid.txt new file mode 100644 index 0000000000000..4e4ff7e3d5f3c --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-invalid.txt @@ -0,0 +1,19 @@ +Harness status: OK + +Found 14 tests + +14 Pass +Pass e.style['scroll-snap-type'] = "auto" should not set the property value +Pass e.style['scroll-snap-type'] = "x y" should not set the property value +Pass e.style['scroll-snap-type'] = "block mandatory inline" should not set the property value +Pass e.style['scroll-snap-type'] = "none both" should not set the property value +Pass e.style['scroll-snap-type'] = "none mandatory" should not set the property value +Pass e.style['scroll-snap-type'] = "both none" should not set the property value +Pass e.style['scroll-snap-type'] = "mandatory" should not set the property value +Pass e.style['scroll-snap-type'] = "proximity" should not set the property value +Pass e.style['scroll-snap-type'] = "mandatory inline" should not set the property value +Pass e.style['scroll-snap-type'] = "proximity both" should not set the property value +Pass e.style['scroll-snap-type'] = "mandatory x" should not set the property value +Pass e.style['scroll-snap-type'] = "proximity y" should not set the property value +Pass e.style['scroll-snap-type'] = "mandatory block" should not set the property value +Pass e.style['scroll-snap-type'] = "proximity mandatory" should not set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-valid.txt new file mode 100644 index 0000000000000..5d4418ec209c4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-valid.txt @@ -0,0 +1,16 @@ +Harness status: OK + +Found 11 tests + +11 Pass +Pass e.style['scroll-snap-type'] = "none" should set the property value +Pass e.style['scroll-snap-type'] = "x" should set the property value +Pass e.style['scroll-snap-type'] = "y" should set the property value +Pass e.style['scroll-snap-type'] = "block" should set the property value +Pass e.style['scroll-snap-type'] = "inline" should set the property value +Pass e.style['scroll-snap-type'] = "both" should set the property value +Pass e.style['scroll-snap-type'] = "y mandatory" should set the property value +Pass e.style['scroll-snap-type'] = "block mandatory" should set the property value +Pass e.style['scroll-snap-type'] = "both mandatory" should set the property value +Pass e.style['scroll-snap-type'] = "inline proximity" should set the property value +Pass e.style['scroll-snap-type'] = "x proximity" should set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/resnap-on-layout-is-immediate.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/resnap-on-layout-is-immediate.txt new file mode 100644 index 0000000000000..1fe1a1ee51c1d --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/resnap-on-layout-is-immediate.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass When re-snap happens on layout, the new scroll position should be set immediately \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/resnap-on-snap-alignment-change.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/resnap-on-snap-alignment-change.txt new file mode 100644 index 0000000000000..ccea59a92f772 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/resnap-on-snap-alignment-change.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Resnap when the current snap position is no longer a valid snap target \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-margin.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-margin.txt new file mode 100644 index 0000000000000..359e09a31a4f8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-margin.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass Snaps to the positions adjusted by scroll-margin +Pass scroll-margin doesn't contribute to the snap position of the element if it's outside of the scroll port \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.txt new file mode 100644 index 0000000000000..91e06547bbcb3 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass snaps to bottom edge of large snap area that doesn't cover the snap port. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-padding-and-margin.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-padding-and-margin.txt new file mode 100644 index 0000000000000..ce167554b9ea1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-padding-and-margin.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass scroll-padding-and-margin +Pass scroll-padding-and-margin 1 \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-padding.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-padding.txt new file mode 100644 index 0000000000000..bba0bcb7c10a1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-padding.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Snaps to the positions adjusted by scroll-padding \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-001.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-001.txt new file mode 100644 index 0000000000000..1659b8dcf6b6b --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-001.txt @@ -0,0 +1,9 @@ +Harness status: OK + +Found 4 tests + +4 Pass +Pass A scroll with intended direction and end position should not pass a snap area with scroll-snap-stop: always. +Pass A scroll with intended end position should always choose the closest snap position regardless of the scroll-snap-stop value. +Pass A scroll outside bounds in the snapping axis with intended direction and end position should not pass a snap area with scroll-snap-stop: always. +Pass A scroll outside bounds in the non-snapping axis with intended direction and end position should not pass a snap area with scroll-snap-stop: always. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-002.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-002.txt new file mode 100644 index 0000000000000..e920b57f45b5a --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-002.txt @@ -0,0 +1,11 @@ +Harness status: OK + +Found 6 tests + +6 Pass +Pass The closest snap point is preferred than scroll-snap-stop: always where it's further than the destination (the closest one is closer to the scroll start position than the destination) +Pass The closest snap point is preferred than scroll-snap-stop: always where it's further than the destination (the closest one is further than the destination from the start position) +Pass The scroll destination on a large element whose snap area covers the snapport entirely is a valid snap position +Pass The scroll destination on a large element whose snap area covers the snapport entirely is a valid snap position (with two `scroll-snap-stop: always` snap points +Pass `scroll-snap-stop: always` snap point is preferred even if the snap area entire snapport +Pass `scroll-snap-stop: always` snap point is further than the scroll destination \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-change.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-change.txt new file mode 100644 index 0000000000000..ec649a72fad93 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-stop-change.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass scroll-snap-stop for areas on HTML should control snapping behavior and changing it takes effect +Pass scroll-snap-stop for areas on DIV should control snapping behavior and changing it takes effect \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-type.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-type.txt new file mode 100644 index 0000000000000..f1914c6c0a255 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scroll-snap-type.txt @@ -0,0 +1,9 @@ +Harness status: OK + +Found 4 tests + +4 Pass +Pass mandatory scroll-snap-type should snap as long as the element is visible. +Pass proximity scroll-snap-type shouldn't snap if the snap position is too far away. +Pass proximity scroll-snap-type should snap if the snap position is close. +Pass none scroll-snap-type shouldn't snap. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scrollTo-scrollBy-snaps.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scrollTo-scrollBy-snaps.txt new file mode 100644 index 0000000000000..5a549ef2e597d --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/scrollTo-scrollBy-snaps.txt @@ -0,0 +1,45 @@ +Harness status: OK + +Found 40 tests + +40 Pass +Pass assign scrollLeft and scrollTop for {left: 800} on div lands on (1000, 0) +Pass assign scrollLeft and scrollTop for {left: 800} on viewport-defining element lands on (1000, 0) +Pass scrollTo({left: 800}) on div lands on (1000, 0) +Pass scrollBy({left: 800}) on div lands on (1000, 0) +Pass scrollTo({left: 800}) on viewport-defining element lands on (1000, 0) +Pass scrollBy({left: 800}) on viewport-defining element lands on (1000, 0) +Pass scrollTo({left: 800}) on window lands on (1000, 0) +Pass scrollBy({left: 800}) on window lands on (1000, 0) +Pass assign scrollLeft and scrollTop for {top: 900} on div lands on (0, 1000) +Pass assign scrollLeft and scrollTop for {top: 900} on viewport-defining element lands on (0, 1000) +Pass scrollTo({top: 900}) on div lands on (0, 1000) +Pass scrollBy({top: 900}) on div lands on (0, 1000) +Pass scrollTo({top: 900}) on viewport-defining element lands on (0, 1000) +Pass scrollBy({top: 900}) on viewport-defining element lands on (0, 1000) +Pass scrollTo({top: 900}) on window lands on (0, 1000) +Pass scrollBy({top: 900}) on window lands on (0, 1000) +Pass assign scrollLeft and scrollTop for {left: 900, top: 800} on div lands on (1000, 1000) +Pass assign scrollLeft and scrollTop for {left: 900, top: 800} on viewport-defining element lands on (1000, 1000) +Pass scrollTo({left: 900, top: 800}) on div lands on (1000, 1000) +Pass scrollBy({left: 900, top: 800}) on div lands on (1000, 1000) +Pass scrollTo({left: 900, top: 800}) on viewport-defining element lands on (1000, 1000) +Pass scrollBy({left: 900, top: 800}) on viewport-defining element lands on (1000, 1000) +Pass scrollTo({left: 900, top: 800}) on window lands on (1000, 1000) +Pass scrollBy({left: 900, top: 800}) on window lands on (1000, 1000) +Pass assign scrollLeft and scrollTop for {left: 800, top: -100} on div lands on (1000, 0) +Pass assign scrollLeft and scrollTop for {left: 800, top: -100} on viewport-defining element lands on (1000, 0) +Pass scrollTo({left: 800, top: -100}) on div lands on (1000, 0) +Pass scrollBy({left: 800, top: -100}) on div lands on (1000, 0) +Pass scrollTo({left: 800, top: -100}) on viewport-defining element lands on (1000, 0) +Pass scrollBy({left: 800, top: -100}) on viewport-defining element lands on (1000, 0) +Pass scrollTo({left: 800, top: -100}) on window lands on (1000, 0) +Pass scrollBy({left: 800, top: -100}) on window lands on (1000, 0) +Pass assign scrollLeft and scrollTop for {left: 10000, top: -100} on div lands on (1000, 0) +Pass assign scrollLeft and scrollTop for {left: 10000, top: -100} on viewport-defining element lands on (1000, 0) +Pass scrollTo({left: 10000, top: -100}) on div lands on (1000, 0) +Pass scrollBy({left: 10000, top: -100}) on div lands on (1000, 0) +Pass scrollTo({left: 10000, top: -100}) on viewport-defining element lands on (1000, 0) +Pass scrollBy({left: 10000, top: -100}) on viewport-defining element lands on (1000, 0) +Pass scrollTo({left: 10000, top: -100}) on window lands on (1000, 0) +Pass scrollBy({left: 10000, top: -100}) on window lands on (1000, 0) \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.txt new file mode 100644 index 0000000000000..3c281bc7dd94b --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Adding a new snap area when there are none should make the scroller snap to it. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.txt new file mode 100644 index 0000000000000..7c038e5c74520 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass Changing a large target's snap alignment shouldn't make the scroller resnap if the scroller is already in a valid snap position. +Pass Changing the current (non-covering) target's snap alignment should make the scroller snap according to the new alignment. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.txt new file mode 100644 index 0000000000000..b5cc57daa1402 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 3 tests + +3 Pass +Pass Removing the current target's snap alignment should make the scroller resnap to a new snap area. +Pass Changing an element snap alignment from none to start should make thescroller resnap. +Pass Changing an element snap alignment from none to start by adding a class should make the scroller resnap. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.txt new file mode 100644 index 0000000000000..8678e9bda3072 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 3 tests + +3 Pass +Pass Changing the scroller's snap type to y should make it resnap on the y-axis. +Pass Changing the scroller's snap type to x should make it resnap on the x-axis. +Pass Changing the scroller's snap type axis should make it resnap. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.txt new file mode 100644 index 0000000000000..8678e9bda3072 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 3 tests + +3 Pass +Pass Changing the scroller's snap type to y should make it resnap on the y-axis. +Pass Changing the scroller's snap type to x should make it resnap on the x-axis. +Pass Changing the scroller's snap type axis should make it resnap. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/move-current-target.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/move-current-target.txt new file mode 100644 index 0000000000000..8fc455388cd9f --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/move-current-target.txt @@ -0,0 +1,9 @@ +Harness status: OK + +Found 4 tests + +4 Pass +Pass Moving the current snap target should make the scroller resnap to it. +Pass Changing the layout of other elements should be able to cause resnapping to the target. +Pass Transforming the current snap target should make the scroller resnap to it. +Pass Applying two property changes that do not change the visual offset of the target should not change the scroll offset. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element-scaled-ancestor.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element-scaled-ancestor.txt new file mode 100644 index 0000000000000..d92f4a328311a --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element-scaled-ancestor.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass out-of-viewport focused element is not the selected snap target under a scaled ancestor. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-snap-target-containing-focused-element.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-snap-target-containing-focused-element.txt new file mode 100644 index 0000000000000..878146e09fefd --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-snap-target-containing-focused-element.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Re-snap prefers snap target containing the focused element when multiple targets are aligned \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/re-snap-focused-target-after-scrollBy.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/re-snap-focused-target-after-scrollBy.txt new file mode 100644 index 0000000000000..a421010a65310 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/re-snap-focused-target-after-scrollBy.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Scroller re-snaps to the focused target after scrollBy \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/remove-current-target.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/remove-current-target.txt new file mode 100644 index 0000000000000..66796b29f204e --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/remove-current-target.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Removing the current snap target should make the scroller snap to a new target. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-on-oveflow-hidden-container.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-on-oveflow-hidden-container.txt new file mode 100644 index 0000000000000..b0fa609c798c1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-on-oveflow-hidden-container.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass resnap-on-oveflow-hidden-container \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.txt new file mode 100644 index 0000000000000..ca16226321d5e --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Resnap to focused element after relayout \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.txt new file mode 100644 index 0000000000000..355371c497d30 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Scroller should snap to at least one of the targets if unable to snap to both after a layout change. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.txt new file mode 100644 index 0000000000000..7adaa136d9670 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Making a snap container not scrollable should promote the next scrollable ancestor to become a snap container. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-at-user-scroll-end.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-at-user-scroll-end.txt new file mode 100644 index 0000000000000..7f8f780f034b4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-at-user-scroll-end.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Tests that window should snap at user scroll end. \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-intended-direction.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-intended-direction.txt new file mode 100644 index 0000000000000..a43d5aa027fcf --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-intended-direction.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass `intended direction` scroll snaps only at points ahead of the scroll direction \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-1.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-1.txt new file mode 100644 index 0000000000000..a9b9aa2159b04 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-1.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Snap to points of combinations of two different elements \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-2.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-2.txt new file mode 100644 index 0000000000000..a9b9aa2159b04 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-2.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Snap to points of combinations of two different elements \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-empty-sized-element.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-empty-sized-element.txt new file mode 100644 index 0000000000000..423dc0482c102 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-empty-sized-element.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Resnap to empty sized element \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-transformed-target.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-transformed-target.txt new file mode 100644 index 0000000000000..b160c76aa0f27 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-transformed-target.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 3 tests + +3 Pass +Pass Snaps to the transformed snap start position +Pass Snaps to the transformed snap end position +Pass Snaps to visible top left position of the transformed box \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both-pseudo.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both-pseudo.txt new file mode 100644 index 0000000000000..d3d6310faf2f8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both-pseudo.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Only snap to visible areas in the case where taking the closest snap point of each axis does not snap to a visible area \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both.txt new file mode 100644 index 0000000000000..d3d6310faf2f8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Only snap to visible areas in the case where taking the closest snap point of each axis does not snap to a visible area \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-both.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-both.txt new file mode 100644 index 0000000000000..311f47d4644d5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-both.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Snap to area such that only the scroll margin from both axes' areas are visible \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.txt new file mode 100644 index 0000000000000..2f583e0542a63 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Scroll margin should be considered when calculating snap area visibilty while snapping on the x-axis \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.txt new file mode 100644 index 0000000000000..3a8cab4fdc288 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Scroll margin should be considered when calculating snap area visibilty while snapping on the y-axis \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-x-axis.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-x-axis.txt new file mode 100644 index 0000000000000..e7f030136daab --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-x-axis.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Only snap to visible area on X axis, even when the non-visible ones are closer \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-y-axis.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-y-axis.txt new file mode 100644 index 0000000000000..4487ead27d344 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/snap-to-visible-areas-y-axis.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Only snap to visible area on Y axis, even when the non-visible ones are closer \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-001.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-001.txt new file mode 100644 index 0000000000000..6c00d80408192 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-001.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Snaps to the positions defined by the element as much as possible \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-002.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-002.txt new file mode 100644 index 0000000000000..79021384ba104 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-002.txt @@ -0,0 +1,9 @@ +Harness status: OK + +Found 4 tests + +4 Pass +Pass Unreachable snap point with `scroll-snap-align: end` +Pass Unreachable snap point with `scroll-snap-align: center` +Pass Unreachable snap point with `scroll-snap-align: end` in RTL +Pass Unreachable snap point with `scroll-snap-align: center` in RTL \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-003.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-003.txt new file mode 100644 index 0000000000000..6c00d80408192 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-scroll-snap/unreachable-snap-positions-003.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Snaps to the positions defined by the element as much as possible \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/cssom-view/smooth-scroll-nonstop.txt b/Tests/LibWeb/Text/expected/wpt-import/css/cssom-view/smooth-scroll-nonstop.txt new file mode 100644 index 0000000000000..dfc033eff1890 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/cssom-view/smooth-scroll-nonstop.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass noop scrollTo doesn't interrupt ongoing smooth scroll. +Pass noop scrollIntoView doesn't interrupt ongoing smooth scroll. \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/async-scrolling/nested-scroller-wheel-prevent-default.html b/Tests/LibWeb/Text/input/async-scrolling/nested-scroller-wheel-prevent-default.html index 7c7b233838ce5..8a76756772b97 100644 --- a/Tests/LibWeb/Text/input/async-scrolling/nested-scroller-wheel-prevent-default.html +++ b/Tests/LibWeb/Text/input/async-scrolling/nested-scroller-wheel-prevent-default.html @@ -34,7 +34,7 @@ wheelEventWasDefaultPrevented = event.defaultPrevented; }, { passive: false }); - println(`async can scroll before wheel: ${internals.asyncScrollingStateCanWheelScrollAt(50, 50, 0, 100, false)}`); + println(`async can scroll before wheel: ${internals.asyncScrollingStateCanWheelScrollAt(50, 50, 0, 100)}`); await internals.wheel(50, 50, 0, 100); diff --git a/Tests/LibWeb/Text/input/async-scrolling/snap-container-wheel-nested-navigable.html b/Tests/LibWeb/Text/input/async-scrolling/snap-container-wheel-nested-navigable.html new file mode 100644 index 0000000000000..6e35aae3e60ed --- /dev/null +++ b/Tests/LibWeb/Text/input/async-scrolling/snap-container-wheel-nested-navigable.html @@ -0,0 +1,42 @@ + + + +
+ +
+
+
+ diff --git a/Tests/LibWeb/Text/input/async-scrolling/snap-container-wheel-target.html b/Tests/LibWeb/Text/input/async-scrolling/snap-container-wheel-target.html new file mode 100644 index 0000000000000..fa60fa346cda6 --- /dev/null +++ b/Tests/LibWeb/Text/input/async-scrolling/snap-container-wheel-target.html @@ -0,0 +1,48 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/async-scrolling/wheel-scroll-admission.html b/Tests/LibWeb/Text/input/async-scrolling/wheel-scroll-admission.html index 2aaf04d4047f9..2dd3888e63e2f 100644 --- a/Tests/LibWeb/Text/input/async-scrolling/wheel-scroll-admission.html +++ b/Tests/LibWeb/Text/input/async-scrolling/wheel-scroll-admission.html @@ -43,7 +43,7 @@ } function canWheel(output, label, x, y, forceStaleWheelEventRegions = false) { - const canScroll = internals.asyncScrollingStateCanWheelScrollAt(x, y, 0, 100, forceStaleWheelEventRegions); + const canScroll = internals.asyncScrollingStateCanWheelScrollAt(x, y, 0, 100, false, forceStaleWheelEventRegions); output.push(`${label}: ${canScroll}`); } diff --git a/Tests/LibWeb/Text/input/include.js b/Tests/LibWeb/Text/input/include.js index 01be4ded07924..c2dba2b9fd1b0 100644 --- a/Tests/LibWeb/Text/input/include.js +++ b/Tests/LibWeb/Text/input/include.js @@ -67,6 +67,28 @@ function withCollectedWrapper(makeAndMark, reacquire, verify) { verify(reacquire()); } +function scrollendEvent(target) { + const { promise, resolve } = Promise.withResolvers(); + target.addEventListener("scrollend", resolve, { once: true }); + return promise; +} + +async function scrollSettled(target, action) { + const scrollend = scrollendEvent(target); + await action(); + await scrollend; +} + +async function scrollOffsetStopsChanging(readScrollOffset) { + let previousScrollOffset = null; + while (previousScrollOffset !== readScrollOffset()) { + previousScrollOffset = readScrollOffset(); + for (let frame = 0; frame < 5; frame++) { + await animationFrame(); + } + } +} + async function waitForImageAnimationState(url, predicate, targetWindow = window) { return new Promise(async resolve => { while (true) { diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-auto-scroll-modes.html b/Tests/LibWeb/Text/input/scroll-snap-after-auto-scroll-modes.html new file mode 100644 index 0000000000000..a026917eb3f64 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-auto-scroll-modes.html @@ -0,0 +1,83 @@ + + + +
+
line1
+
line2
+
line3
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-flick.html b/Tests/LibWeb/Text/input/scroll-snap-after-flick.html new file mode 100644 index 0000000000000..89a6fb5720a18 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-flick.html @@ -0,0 +1,79 @@ + + + +
+
+
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-gesture-settles-during-layout.html b/Tests/LibWeb/Text/input/scroll-snap-after-gesture-settles-during-layout.html new file mode 100644 index 0000000000000..f5b81ecc7667a --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-gesture-settles-during-layout.html @@ -0,0 +1,51 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-gesture-whose-end-is-never-reported.html b/Tests/LibWeb/Text/input/scroll-snap-after-gesture-whose-end-is-never-reported.html new file mode 100644 index 0000000000000..32fb8fbed2231 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-gesture-whose-end-is-never-reported.html @@ -0,0 +1,46 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-homing-keys.html b/Tests/LibWeb/Text/input/scroll-snap-after-homing-keys.html new file mode 100644 index 0000000000000..7242c7aa148e4 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-homing-keys.html @@ -0,0 +1,40 @@ + + + +
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-keyboard-scroll-of-smooth-viewport.html b/Tests/LibWeb/Text/input/scroll-snap-after-keyboard-scroll-of-smooth-viewport.html new file mode 100644 index 0000000000000..1a12e8a872ebd --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-keyboard-scroll-of-smooth-viewport.html @@ -0,0 +1,37 @@ + + + +
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-paging-keys.html b/Tests/LibWeb/Text/input/scroll-snap-after-paging-keys.html new file mode 100644 index 0000000000000..e5f0e0c3b30ea --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-paging-keys.html @@ -0,0 +1,62 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-pan-adopted-following-key-scroll.html b/Tests/LibWeb/Text/input/scroll-snap-after-pan-adopted-following-key-scroll.html new file mode 100644 index 0000000000000..3745ebb8fe13a --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-pan-adopted-following-key-scroll.html @@ -0,0 +1,39 @@ + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-repeated-keyboard-scroll.html b/Tests/LibWeb/Text/input/scroll-snap-after-repeated-keyboard-scroll.html new file mode 100644 index 0000000000000..0476bef3e1ed6 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-repeated-keyboard-scroll.html @@ -0,0 +1,46 @@ + + + +
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-after-scrollbar-drag.html b/Tests/LibWeb/Text/input/scroll-snap-after-scrollbar-drag.html new file mode 100644 index 0000000000000..fb0eef4ffe5ae --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-after-scrollbar-drag.html @@ -0,0 +1,65 @@ + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-align-writing-mode.html b/Tests/LibWeb/Text/input/scroll-snap-align-writing-mode.html new file mode 100644 index 0000000000000..ae649bb42189b --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-align-writing-mode.html @@ -0,0 +1,48 @@ + + + +
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-at-reported-gesture-end.html b/Tests/LibWeb/Text/input/scroll-snap-at-reported-gesture-end.html new file mode 100644 index 0000000000000..3d1490a6308f1 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-at-reported-gesture-end.html @@ -0,0 +1,43 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-consecutive-wheel-steps.html b/Tests/LibWeb/Text/input/scroll-snap-consecutive-wheel-steps.html new file mode 100644 index 0000000000000..789340e337821 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-consecutive-wheel-steps.html @@ -0,0 +1,57 @@ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-directional-wheel-scroll.html b/Tests/LibWeb/Text/input/scroll-snap-directional-wheel-scroll.html new file mode 100644 index 0000000000000..2a0279e6c03c6 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-directional-wheel-scroll.html @@ -0,0 +1,55 @@ + + + +
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-in-pseudo-element-scroll-container.html b/Tests/LibWeb/Text/input/scroll-snap-in-pseudo-element-scroll-container.html new file mode 100644 index 0000000000000..0033f7312faab --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-in-pseudo-element-scroll-container.html @@ -0,0 +1,44 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-new-gesture-during-compositor-snap-scroll.html b/Tests/LibWeb/Text/input/scroll-snap-new-gesture-during-compositor-snap-scroll.html new file mode 100644 index 0000000000000..c92b363f6e1a0 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-new-gesture-during-compositor-snap-scroll.html @@ -0,0 +1,46 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-new-gesture-during-snap-scroll.html b/Tests/LibWeb/Text/input/scroll-snap-new-gesture-during-snap-scroll.html new file mode 100644 index 0000000000000..e498ff3bfcb47 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-new-gesture-during-snap-scroll.html @@ -0,0 +1,48 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-resnap-inside-reused-subtree.html b/Tests/LibWeb/Text/input/scroll-snap-resnap-inside-reused-subtree.html new file mode 100644 index 0000000000000..304223a5f1cf7 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-resnap-inside-reused-subtree.html @@ -0,0 +1,65 @@ + + + +
+
+
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-scroll-step-while-pinch-zoomed.html b/Tests/LibWeb/Text/input/scroll-snap-scroll-step-while-pinch-zoomed.html new file mode 100644 index 0000000000000..27a88eab1b961 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-scroll-step-while-pinch-zoomed.html @@ -0,0 +1,44 @@ + + + +
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-settlement-during-programmatic-scroll.html b/Tests/LibWeb/Text/input/scroll-snap-settlement-during-programmatic-scroll.html new file mode 100644 index 0000000000000..8ecfff9b77cc5 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-settlement-during-programmatic-scroll.html @@ -0,0 +1,47 @@ + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-spaced-wheel-steps.html b/Tests/LibWeb/Text/input/scroll-snap-spaced-wheel-steps.html new file mode 100644 index 0000000000000..e0925488ec7b1 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-spaced-wheel-steps.html @@ -0,0 +1,58 @@ + + + +
+
+
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-stop-always-during-pan.html b/Tests/LibWeb/Text/input/scroll-snap-stop-always-during-pan.html new file mode 100644 index 0000000000000..a9adc91b92000 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-stop-always-during-pan.html @@ -0,0 +1,58 @@ + + + +
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-stop-wheel-step.html b/Tests/LibWeb/Text/input/scroll-snap-stop-wheel-step.html new file mode 100644 index 0000000000000..cfb51db38411f --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-stop-wheel-step.html @@ -0,0 +1,40 @@ + + + +
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-wheel-step-chained-to-snap-container.html b/Tests/LibWeb/Text/input/scroll-snap-wheel-step-chained-to-snap-container.html new file mode 100644 index 0000000000000..5af735cdd01a3 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-wheel-step-chained-to-snap-container.html @@ -0,0 +1,48 @@ + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-wheel-step-during-programmatic-scroll.html b/Tests/LibWeb/Text/input/scroll-snap-wheel-step-during-programmatic-scroll.html new file mode 100644 index 0000000000000..47f1f858a5dad --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-wheel-step-during-programmatic-scroll.html @@ -0,0 +1,50 @@ + + + +
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-wheel-step-in-one-axis.html b/Tests/LibWeb/Text/input/scroll-snap-wheel-step-in-one-axis.html new file mode 100644 index 0000000000000..e4386dd41385a --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-wheel-step-in-one-axis.html @@ -0,0 +1,50 @@ + + + +
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/scroll-snap-wheel-step-past-nested-navigable-extent.html b/Tests/LibWeb/Text/input/scroll-snap-wheel-step-past-nested-navigable-extent.html new file mode 100644 index 0000000000000..5a02bca51cf30 --- /dev/null +++ b/Tests/LibWeb/Text/input/scroll-snap-wheel-step-past-nested-navigable-extent.html @@ -0,0 +1,35 @@ + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.html new file mode 100644 index 0000000000000..db045965a6e9a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.html @@ -0,0 +1,57 @@ + +Ignore snap points orthogonal to scroll snap axis + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/inheritance.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/inheritance.html new file mode 100644 index 0000000000000..c65e271a3a375 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/inheritance.html @@ -0,0 +1,39 @@ + + + + +Inheritance of CSS Scroll Snap properties + + + + + + + + +
+
+
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/keyboard.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/keyboard.html new file mode 100644 index 0000000000000..c6e28f6eeab83 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/keyboard.html @@ -0,0 +1,182 @@ + + +Arrow key scroll snapping + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/mouse-wheel.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/mouse-wheel.html new file mode 100644 index 0000000000000..fe5beecb912f4 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/mouse-wheel.html @@ -0,0 +1,66 @@ + + +Mouse-wheel scroll snapping speed + + + + + + + + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.html new file mode 100644 index 0000000000000..78842ee599f6a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.html @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + +
+
+
Header 1
+ +
+ +
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/no-snap-position.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/no-snap-position.html new file mode 100644 index 0000000000000..c81771ca9873d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/no-snap-position.html @@ -0,0 +1,89 @@ + + + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/overflowing-snap-areas.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/overflowing-snap-areas.html new file mode 100644 index 0000000000000..bc73f4892d09d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/overflowing-snap-areas.html @@ -0,0 +1,180 @@ + + + + + +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/overscroll-snap.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/overscroll-snap.html new file mode 100644 index 0000000000000..aed6aaa5ad41b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/overscroll-snap.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-computed.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-computed.html new file mode 100644 index 0000000000000..a8321efc1f10d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-computed.html @@ -0,0 +1,26 @@ + + + + +CSS Scroll Snap: getComputedStyle().scrollSnapAlign + + + + + + + + +
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-invalid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-invalid.html new file mode 100644 index 0000000000000..37fd4ce91c1f8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-invalid.html @@ -0,0 +1,21 @@ + + + + +CSS Scroll Snap Test: scroll-snap-align with invalid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-valid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-valid.html new file mode 100644 index 0000000000000..c030d343db6c9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-align-valid.html @@ -0,0 +1,25 @@ + + + + +CSS Scroll Snap Test: scroll-snap-align with valid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-computed.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-computed.html new file mode 100644 index 0000000000000..06b4ec5490a60 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-computed.html @@ -0,0 +1,20 @@ + + + + +CSS Scroll Snap: getComputedStyle().scrollSnapStop + + + + + + + + +
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.html new file mode 100644 index 0000000000000..408dd482d06bd --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.html @@ -0,0 +1,19 @@ + + + + +CSS Scroll Snap Test: scroll-snap-stop with invalid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-valid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-valid.html new file mode 100644 index 0000000000000..f1f60801b7e8c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-stop-valid.html @@ -0,0 +1,18 @@ + + + + +CSS Scroll Snap Test: scroll-snap-stop with valid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-computed.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-computed.html new file mode 100644 index 0000000000000..3f01174076e50 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-computed.html @@ -0,0 +1,27 @@ + + + + +CSS Scroll Snap: getComputedStyle().scrollSnapType + + + + + + + +
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-invalid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-invalid.html new file mode 100644 index 0000000000000..62d027b803779 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-invalid.html @@ -0,0 +1,32 @@ + + + + +CSS Scroll Snap Test: scroll-snap-type with invalid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-valid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-valid.html new file mode 100644 index 0000000000000..8f6b2727d1bd2 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/parsing/scroll-snap-type-valid.html @@ -0,0 +1,29 @@ + + + + +CSS Scroll Snap Test: scroll-snap-type with valid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/resnap-on-layout-is-immediate.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/resnap-on-layout-is-immediate.html new file mode 100644 index 0000000000000..99d7e528d54ee --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/resnap-on-layout-is-immediate.html @@ -0,0 +1,48 @@ + + + + + + +
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/resnap-on-snap-alignment-change.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/resnap-on-snap-alignment-change.html new file mode 100644 index 0000000000000..6b9425fed0ca9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/resnap-on-snap-alignment-change.html @@ -0,0 +1,48 @@ + + + + + Resnap when the current snap position is no longer a valid snap target. + + + + + + + +
ONE
+
TWO
+
THREE
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-margin.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-margin.html new file mode 100644 index 0000000000000..dcf955a074865 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-margin.html @@ -0,0 +1,90 @@ + + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.html new file mode 100644 index 0000000000000..ed13cf5bbd6d7 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.html @@ -0,0 +1,88 @@ + + + A test case that scrolling to a point on large element where the snap area + doesn't cover over the snapport + + + + + +
+
+
1
+
2
+
3
+
4
+
5
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-padding-and-margin.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-padding-and-margin.html new file mode 100644 index 0000000000000..3d8a319cea729 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-padding-and-margin.html @@ -0,0 +1,51 @@ + + + + + + + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-padding.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-padding.html new file mode 100644 index 0000000000000..c5491bafa7b49 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-padding.html @@ -0,0 +1,49 @@ + + + + + + +
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-001.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-001.html new file mode 100644 index 0000000000000..4a1d06bf8ae14 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-001.html @@ -0,0 +1,96 @@ + + + + + + +
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-002.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-002.html new file mode 100644 index 0000000000000..3b403390511df --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-002.html @@ -0,0 +1,213 @@ + + + + + + + + +
+
+
+ +
+ +
+
+ + +
+
+
+
+ +
+
+ + + +
+
+
+
+ +
+ +
+
+ + +
+
+
+
+ +
+ +
+
+ + +
+
+
+
+ +
+ +
+
+ + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-change.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-change.html new file mode 100644 index 0000000000000..45017b230fe00 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-stop-change.html @@ -0,0 +1,90 @@ + + + + + + + + +
+
+
+
+ +
+
+
+
+
+
+
+ +
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-type.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-type.html new file mode 100644 index 0000000000000..d4e708b6f4a70 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scroll-snap-type.html @@ -0,0 +1,87 @@ + + + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scrollTo-scrollBy-snaps.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scrollTo-scrollBy-snaps.html new file mode 100644 index 0000000000000..7dfe7950c24b8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/scrollTo-scrollBy-snaps.html @@ -0,0 +1,160 @@ + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.html new file mode 100644 index 0000000000000..b44c08de38ff9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.html @@ -0,0 +1,55 @@ + + + Adding a new snap area when there are none should make the scroller snap to it. + + + + + + +
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.html new file mode 100644 index 0000000000000..c22a7a2c34d2e --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.html @@ -0,0 +1,119 @@ + + + Updating the snap alignment of a snap container's content should make the snap + container resnap accordingly. + + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html new file mode 100644 index 0000000000000..4988af56da6bc --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html @@ -0,0 +1,108 @@ + + + Updating the snap alignment of a snap container's content should make the snap + container resnap accordingly. + + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.html new file mode 100644 index 0000000000000..59c9f5ef2b42b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.html @@ -0,0 +1,95 @@ + + + + Updating the scroll-snap-type of the root element should make it resnap accordingly. + This is another vairant of changing-scroll-snap-type.html for the root element. + + + + + + +
+
+
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html new file mode 100644 index 0000000000000..e72f29e7aec05 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html @@ -0,0 +1,96 @@ + + + Updating the scroll-snap-type of a snap container should make it resnap accordingly. + + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/move-current-target.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/move-current-target.html new file mode 100644 index 0000000000000..7aa786f50023a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/move-current-target.html @@ -0,0 +1,116 @@ + + + Moving the current snap target should make the scroller resnap to it. + + + + + + +
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element-scaled-ancestor.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element-scaled-ancestor.html new file mode 100644 index 0000000000000..ea1586172b6ff --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element-scaled-ancestor.html @@ -0,0 +1,93 @@ + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-snap-target-containing-focused-element.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-snap-target-containing-focused-element.html new file mode 100644 index 0000000000000..cd027a17dce36 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-snap-target-containing-focused-element.html @@ -0,0 +1,60 @@ + + + + + + + +
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/re-snap-focused-target-after-scrollBy.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/re-snap-focused-target-after-scrollBy.html new file mode 100644 index 0000000000000..13ee5dcf5e83d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/re-snap-focused-target-after-scrollBy.html @@ -0,0 +1,80 @@ + + + + +Scroll snap: re-snap to the focused target after scrollBy + + + + + + +
+
+
top
+
first
+
second
+
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/resources/common.js b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/resources/common.js new file mode 100644 index 0000000000000..b4d0dbcb64f1f --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/resources/common.js @@ -0,0 +1,161 @@ +// Utility functions for scroll snap tests which verify User-Agents' snap point +// selection logic when multiple snap targets are aligned. +// It depends on methods in /resources/testdriver-actions.js and +// /dom/event/scrolling/scroll_support.js so html files using these functions +// should include those files as + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-on-oveflow-hidden-container.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-on-oveflow-hidden-container.html new file mode 100644 index 0000000000000..83056239aefb8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-on-oveflow-hidden-container.html @@ -0,0 +1,63 @@ + + + + + + + +
+
1
+
2
+
3
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.html new file mode 100644 index 0000000000000..de039b5d9e239 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.html @@ -0,0 +1,82 @@ + +Resnap to focused element after relayout + + + + + + + +
+
+
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.html new file mode 100644 index 0000000000000..2b606f53f5aa1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.html @@ -0,0 +1,91 @@ + + + The scroller should try to resnap to targets for both axes if possible. + + + + + + +
+
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.html new file mode 100644 index 0000000000000..2de86835c2cb4 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.html @@ -0,0 +1,128 @@ + + + When an element no longer captures snap positions (e.g., no longer + scrollable), then its currently captured snap areas must be reassigned. + + + + + + +
+
+
+
+
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-at-user-scroll-end.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-at-user-scroll-end.html new file mode 100644 index 0000000000000..fe492646449bb --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-at-user-scroll-end.html @@ -0,0 +1,64 @@ + + + +Tests that window should snap at user scroll end. + + + + + + + + +
+
+ +
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-intended-direction.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-intended-direction.html new file mode 100644 index 0000000000000..c2efe1ecf20b6 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-intended-direction.html @@ -0,0 +1,48 @@ + +`intended direction` scroll snaps only at points ahead of the scroll direction + + + + + + +
+
+
1
+
2
+
3
+
4
+
5
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-1.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-1.html new file mode 100644 index 0000000000000..9965d9406a9fd --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-1.html @@ -0,0 +1,79 @@ + + + + Snap to points of combinations of two different elements + + + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-2.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-2.html new file mode 100644 index 0000000000000..bfd057fb505b1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-combination-of-two-elements-2.html @@ -0,0 +1,87 @@ + + + + Snap to points of combinations of two different elements + + + + + +
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-empty-sized-element.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-empty-sized-element.html new file mode 100644 index 0000000000000..859d3a68fb679 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-empty-sized-element.html @@ -0,0 +1,51 @@ + + +Resnap to empty sized element + + + + + +
    +
  • +
  • +
  • +
  • +
  • +
  • +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-transformed-target.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-transformed-target.html new file mode 100644 index 0000000000000..5fb186eeef610 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-transformed-target.html @@ -0,0 +1,56 @@ + + + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both-pseudo.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both-pseudo.html new file mode 100644 index 0000000000000..c044363b71a50 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both-pseudo.html @@ -0,0 +1,93 @@ + + + Snap to a visible area only even when there is a closer snap point for an area + that is closer but not visible (using both axes snap type), where the relevant + snap areas are pseudo-elements + + + + + +
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both.html new file mode 100644 index 0000000000000..1a96eeb714edb --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-both.html @@ -0,0 +1,70 @@ + + + Snap to a visible area only even when there is a closer snap point for an area + that is closer but not visible (using both axes snap type) + + + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-both.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-both.html new file mode 100644 index 0000000000000..778adeb611285 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-both.html @@ -0,0 +1,80 @@ + + + Snap to an area where the element's scroll-margin is visible but not the + element itself (using both axes snap type) + + + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.html new file mode 100644 index 0000000000000..b24228e171620 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.html @@ -0,0 +1,69 @@ + + + Snap to an area where the element's scroll-margin is visible but not the + element itself (using x-axis snap type) + + + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.html new file mode 100644 index 0000000000000..e925b7435f5e2 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.html @@ -0,0 +1,69 @@ + + + Snap to an area where the element's scroll-margin is visible but not the + element itself (using y-axis snap type) + + + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-x-axis.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-x-axis.html new file mode 100644 index 0000000000000..8b8747f44604c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-x-axis.html @@ -0,0 +1,66 @@ + + + Snap to a visible area only even when there is a closer snap point for an area + that is closer but not visible (using x-axis snap type) + + + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-y-axis.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-y-axis.html new file mode 100644 index 0000000000000..e4fa1a284fa1d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/snap-to-visible-areas-y-axis.html @@ -0,0 +1,66 @@ + + + Snap to a visible area only even when there is a closer snap point for an area + that is closer but not visible (using y-axis snap type) + + + + + +
+
+
+
+
+
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/support/common.css b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/support/common.css new file mode 100644 index 0000000000000..f49c7cbacd5bd --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/support/common.css @@ -0,0 +1,44 @@ +body { + margin: 0; +} + +#scroller { + position: absolute; + width: 400px; + height: 400px; + overflow: scroll; + padding: 0; + + scroll-snap-type: both mandatory; +} + +.snap { + position: absolute; + width: 200px; + height: 200px; + background-color: blue; + + scroll-snap-align: start; +} + +#space { + position: absolute; + width: 1000px; + height: 1000px; +} + +.left { + left: 0; +} + +.top { + top: 0; +} + +.right { + left: 400px; +} + +.bottom { + top: 400px; +} \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-001.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-001.html new file mode 100644 index 0000000000000..b43c0ba4ff2ff --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-001.html @@ -0,0 +1,53 @@ + + + + + + +
+
+
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-002.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-002.html new file mode 100644 index 0000000000000..4b79ddd1f48d6 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-002.html @@ -0,0 +1,67 @@ + + + + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-003.html b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-003.html new file mode 100644 index 0000000000000..a55aa3bb43d9b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-scroll-snap/unreachable-snap-positions-003.html @@ -0,0 +1,39 @@ + + + + + + + + +
+ +
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/cssom-view/smooth-scroll-nonstop.html b/Tests/LibWeb/Text/input/wpt-import/css/cssom-view/smooth-scroll-nonstop.html new file mode 100644 index 0000000000000..037346945f27f --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/cssom-view/smooth-scroll-nonstop.html @@ -0,0 +1,92 @@ + + + +Noop smooth scrolls don't interrupt ongoing smooth scrolls + + + + + + + + +
+
+
+
+ + + diff --git a/UI/Android/src/main/cpp/WebViewImplementationNative.cpp b/UI/Android/src/main/cpp/WebViewImplementationNative.cpp index 6cf5b1f3f4efb..923d5eb004adc 100644 --- a/UI/Android/src/main/cpp/WebViewImplementationNative.cpp +++ b/UI/Android/src/main/cpp/WebViewImplementationNative.cpp @@ -113,6 +113,9 @@ void WebViewImplementationNative::mouse_event(Web::MouseEvent::Type event_type, Web::UIEvents::KeyModifier::Mod_None, 0, 0, + Web::WheelDeltaPrecision::Discrete, + Web::ScrollGesturePhase::None, + 0, nullptr }; diff --git a/UI/AppKit/Interface/Event.mm b/UI/AppKit/Interface/Event.mm index c0a4a5c56a4e9..0ac5fabcada8a 100644 --- a/UI/AppKit/Interface/Event.mm +++ b/UI/AppKit/Interface/Event.mm @@ -37,6 +37,21 @@ return static_cast(modifiers); } +static Web::ScrollGesturePhase ns_scroll_event_to_scroll_gesture_phase(NSEvent* event) +{ + // Fingers resting on the touchpad without moving continue the gesture they began. + static constexpr NSEventPhase ongoing_phases = NSEventPhaseMayBegin | NSEventPhaseBegan | NSEventPhaseChanged | NSEventPhaseStationary; + static constexpr NSEventPhase ending_phases = NSEventPhaseEnded | NSEventPhaseCancelled; + + if ((event.phase & ending_phases) != 0 || (event.momentumPhase & ending_phases) != 0) + return Web::ScrollGesturePhase::Ended; + if ((event.momentumPhase & ongoing_phases) != 0) + return Web::ScrollGesturePhase::Momentum; + if ((event.phase & ongoing_phases) != 0) + return Web::ScrollGesturePhase::Ongoing; + return Web::ScrollGesturePhase::None; +} + Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type type, NSEvent* event, NSView* view, Web::UIEvents::MouseButton button) { auto position = [view convertPoint:event.locationInWindow fromView:nil]; @@ -49,24 +64,30 @@ double wheel_delta_x = 0; double wheel_delta_y = 0; + auto wheel_delta_precision = Web::WheelDeltaPrecision::Discrete; + auto scroll_gesture_phase = Web::ScrollGesturePhase::None; if (type == Web::MouseEvent::Type::MouseWheel) { wheel_delta_x = -[event scrollingDeltaX]; wheel_delta_y = -[event scrollingDeltaY]; - if (![event hasPreciseScrollingDeltas]) { + if ([event hasPreciseScrollingDeltas]) { + wheel_delta_precision = Web::WheelDeltaPrecision::Precise; + } else { static constexpr double imprecise_scroll_multiplier = 40; wheel_delta_x *= imprecise_scroll_multiplier; wheel_delta_y *= imprecise_scroll_multiplier; } + + scroll_gesture_phase = ns_scroll_event_to_scroll_gesture_phase(event); } int click_count = 0; if (type == Web::MouseEvent::Type::MouseDown || type == Web::MouseEvent::Type::MouseUp) click_count = static_cast(event.clickCount); - return { type, device_position, device_screen_position, button, button, modifiers, wheel_delta_x, wheel_delta_y, click_count, nullptr }; + return { type, device_position, device_screen_position, button, button, modifiers, wheel_delta_x, wheel_delta_y, wheel_delta_precision, scroll_gesture_phase, click_count, nullptr }; } struct DragData : public Web::BrowserInputData { diff --git a/UI/AppKit/Interface/LadybirdWebView.mm b/UI/AppKit/Interface/LadybirdWebView.mm index 947487b5602aa..b9f3f865c8598 100644 --- a/UI/AppKit/Interface/LadybirdWebView.mm +++ b/UI/AppKit/Interface/LadybirdWebView.mm @@ -1306,7 +1306,7 @@ - (void)mouseExited:(NSEvent*)event return; } - Web::MouseEvent mouse_event { Web::MouseEvent::Type::MouseLeave, {}, {}, Web::UIEvents::MouseButton::None, Web::UIEvents::MouseButton::None, Web::UIEvents::KeyModifier::Mod_None, 0, 0, 0, nullptr }; + Web::MouseEvent mouse_event { Web::MouseEvent::Type::MouseLeave, {}, {}, Web::UIEvents::MouseButton::None, Web::UIEvents::MouseButton::None, Web::UIEvents::KeyModifier::Mod_None, 0, 0, Web::WheelDeltaPrecision::Discrete, Web::ScrollGesturePhase::None, 0, nullptr }; m_web_view_bridge->enqueue_input_event(move(mouse_event)); } diff --git a/UI/Qt/WebContentView.cpp b/UI/Qt/WebContentView.cpp index 15ee7d0563216..1b99402fdb45e 100644 --- a/UI/Qt/WebContentView.cpp +++ b/UI/Qt/WebContentView.cpp @@ -347,20 +347,49 @@ static QPointF wheel_delta_from_angle_delta(QPoint angle_delta) return { step_x * scroll_step_size, step_y * scroll_step_size }; } -static QPointF wheel_delta_from_qt_event(QWheelEvent const& wheel_event) +struct WheelDelta { + QPointF delta; + Web::WheelDeltaPrecision precision { Web::WheelDeltaPrecision::Discrete }; +}; + +static bool wheel_event_scrolls_continuously(QWheelEvent const& wheel_event) { - auto pixel_delta = -wheel_event.pixelDelta(); + if (wheel_event.phase() != Qt::NoScrollPhase) + return true; + // Some platforms deliver touchpad scrolling without scroll phases, so fall back to the type of the device. auto const* pointing_device = wheel_event.pointingDevice(); - // NB: macOS can report a tiny pixel delta for mouse-wheel ticks. Use it only for touchpads so physical wheels - // continue through the line-step conversion below. - if (!pixel_delta.isNull() && pointing_device && pointing_device->type() == QInputDevice::DeviceType::TouchPad) - return pixel_delta; + return pointing_device && pointing_device->type() == QInputDevice::DeviceType::TouchPad; +} + +static WheelDelta wheel_delta_from_qt_event(QWheelEvent const& wheel_event) +{ + auto pixel_delta = -wheel_event.pixelDelta(); + // NB: macOS can report a tiny pixel delta for mouse-wheel ticks. Use it only for continuous scrolling so physical + // wheels continue through the line-step conversion below. + if (!pixel_delta.isNull() && wheel_event_scrolls_continuously(wheel_event)) + return { pixel_delta, Web::WheelDeltaPrecision::Precise }; auto angle_delta = -wheel_event.angleDelta(); if (!angle_delta.isNull()) - return wheel_delta_from_angle_delta(angle_delta); + return { wheel_delta_from_angle_delta(angle_delta), Web::WheelDeltaPrecision::Discrete }; + + return { pixel_delta, Web::WheelDeltaPrecision::Precise }; +} - return pixel_delta; +static Web::ScrollGesturePhase scroll_gesture_phase_from_qt_event(QWheelEvent const& wheel_event) +{ + switch (wheel_event.phase()) { + case Qt::ScrollBegin: + case Qt::ScrollUpdate: + return Web::ScrollGesturePhase::Ongoing; + case Qt::ScrollMomentum: + return Web::ScrollGesturePhase::Momentum; + case Qt::ScrollEnd: + return Web::ScrollGesturePhase::Ended; + case Qt::NoScrollPhase: + break; + } + return Web::ScrollGesturePhase::None; } static Web::UIEvents::KeyCode get_keycode_from_qt_key_event(QKeyEvent const& event) @@ -1358,15 +1387,19 @@ void WebContentView::enqueue_native_event(Web::MouseEvent::Type type, QSinglePoi double wheel_delta_x = 0; double wheel_delta_y = 0; + auto wheel_delta_precision = Web::WheelDeltaPrecision::Discrete; + auto scroll_gesture_phase = Web::ScrollGesturePhase::None; if (type == Web::MouseEvent::Type::MouseWheel) { auto const& wheel_event = static_cast(event); auto wheel_delta = wheel_delta_from_qt_event(wheel_event); - wheel_delta_x = wheel_delta.x(); - wheel_delta_y = wheel_delta.y(); + wheel_delta_x = wheel_delta.delta.x(); + wheel_delta_y = wheel_delta.delta.y(); + wheel_delta_precision = wheel_delta.precision; + scroll_gesture_phase = scroll_gesture_phase_from_qt_event(wheel_event); } - enqueue_input_event(Web::MouseEvent { type, position, screen_position.to_type(), button, buttons, modifiers, wheel_delta_x, wheel_delta_y, m_click_count, nullptr }); + enqueue_input_event(Web::MouseEvent { type, position, screen_position.to_type(), button, buttons, modifiers, wheel_delta_x, wheel_delta_y, wheel_delta_precision, scroll_gesture_phase, m_click_count, nullptr }); } struct DragData : Web::BrowserInputData {