From 017b546373be4dba069bb0fbefc45704fbd0ff99 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 6 Aug 2026 04:22:07 +0100 Subject: [PATCH 1/6] LibWeb: Derive scroll offset bounds from scrollable overflow geometry The scroll offset range was previously clamped to begin at zero. A scroll container whose scrollable overflow extends towards the inline start requires negative offsets to reach that content. Compute both bounds from the position of the scrollable overflow rectangle relative to the scrollport. --- Libraries/LibWeb/HTML/LocalNavigable.cpp | 18 ++++---- Libraries/LibWeb/HTML/Window.cpp | 34 +++++++++------ .../LibWeb/Layout/ScrollableOverflow.cpp | 30 +++++--------- Libraries/LibWeb/Layout/ScrollableOverflow.h | 7 ++++ Libraries/LibWeb/Painting/Paintable.cpp | 41 +++++++++++++++---- Libraries/LibWeb/Painting/Paintable.h | 2 + .../input-scrolls-caret-width-into-view.txt | 2 +- ...lable-overflow-webkit-box-wrap-reverse.txt | 2 + .../input-scrolls-caret-width-into-view.html | 2 +- ...able-overflow-webkit-box-wrap-reverse.html | 39 ++++++++++++++++++ 10 files changed, 126 insertions(+), 51 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/scrollable-overflow-webkit-box-wrap-reverse.txt create mode 100644 Tests/LibWeb/Text/input/scrollable-overflow-webkit-box-wrap-reverse.html diff --git a/Libraries/LibWeb/HTML/LocalNavigable.cpp b/Libraries/LibWeb/HTML/LocalNavigable.cpp index eb99a3b6d788..7324aae64a52 100644 --- a/Libraries/LibWeb/HTML/LocalNavigable.cpp +++ b/Libraries/LibWeb/HTML/LocalNavigable.cpp @@ -3869,14 +3869,13 @@ void LocalNavigable::clamp_viewport_scroll_offset() return; if (!document->paintable_box()) return; - auto scrollable_overflow_rect = document->paintable_box()->scrollable_overflow_rect(); - if (!scrollable_overflow_rect.has_value()) + if (!document->paintable_box()->scrollable_overflow_rect().has_value()) return; - auto max_x = scrollable_overflow_rect->width() - m_viewport_size.width(); - auto max_y = scrollable_overflow_rect->height() - m_viewport_size.height(); + auto minimum_scroll_offset = document->paintable_box()->minimum_scroll_offset(); + auto maximum_scroll_offset = document->paintable_box()->maximum_scroll_offset(); CSSPixelPoint clamped = { - max(CSSPixels(0), min(m_viewport_scroll_offset.x(), max_x)), - max(CSSPixels(0), min(m_viewport_scroll_offset.y(), max_y)), + clamp(m_viewport_scroll_offset.x(), minimum_scroll_offset.x(), maximum_scroll_offset.x()), + clamp(m_viewport_scroll_offset.y(), minimum_scroll_offset.y(), maximum_scroll_offset.y()), }; if (clamped != m_viewport_scroll_offset) perform_scroll_of_viewport_scrolling_box(clamped); @@ -5133,11 +5132,12 @@ GC::Ref LocalNavigable::perform_a_scroll_of_the_viewport(CSSPix // NB: Must update layout before accessing paintables. doc->update_layout(DOM::UpdateLayoutReason::NavigableViewportScroll); - auto scrolling_area = doc->paintable_box()->scrollable_overflow_rect()->to_type(); + auto minimum_scroll_offset = doc->paintable_box()->minimum_scroll_offset().to_type(); + auto maximum_scroll_offset = doc->paintable_box()->maximum_scroll_offset().to_type(); auto new_viewport_scroll_offset = m_viewport_scroll_offset.to_type() + Gfx::Point(layout_dx, layout_dy); // NOTE: Clamp to the scrolling area. - new_viewport_scroll_offset.set_x(max(0.0, min(new_viewport_scroll_offset.x(), scrolling_area.width() - viewport_size().width().to_double()))); - new_viewport_scroll_offset.set_y(max(0.0, min(new_viewport_scroll_offset.y(), scrolling_area.height() - viewport_size().height().to_double()))); + new_viewport_scroll_offset.set_x(clamp(new_viewport_scroll_offset.x(), minimum_scroll_offset.x(), maximum_scroll_offset.x())); + new_viewport_scroll_offset.set_y(clamp(new_viewport_scroll_offset.y(), minimum_scroll_offset.y(), maximum_scroll_offset.y())); auto scroll_promise = perform_a_scroll_of_a_scrolling_box({ .node_id = doc->unique_id(), diff --git a/Libraries/LibWeb/HTML/Window.cpp b/Libraries/LibWeb/HTML/Window.cpp index 581f2ac2e819..66c2c58b454b 100644 --- a/Libraries/LibWeb/HTML/Window.cpp +++ b/Libraries/LibWeb/HTML/Window.cpp @@ -81,6 +81,7 @@ #include #include #include +#include #include #include #include @@ -1723,20 +1724,27 @@ void Window::scroll(ScrollToOptions const& options, GC::Ptr pro VERIFY(document->paintable_box()); auto scrolling_area = document->paintable_box()->scrollable_overflow_rect()->to_type(); + auto overflow_directions = Layout::physical_overflow_directions(*document->layout_node()); + + // 7. -> If the viewport has rightward overflow direction + // Let x be max(0, min(x, viewport scrolling area width - viewport width)). + // -> If the viewport has leftward overflow direction + // Let x be min(0, max(x, viewport width - viewport scrolling area width)). + if (overflow_directions.horizontal_axis_is_positive) { + x = max(0.0f, min(x, scrolling_area.width() - viewport_width)); + } else { + x = min(0.0f, max(x, viewport_width - scrolling_area.width())); + } - // 7. FIXME: For now we always assume overflow direction is rightward - // -> If the viewport has rightward overflow direction - // Let x be max(0, min(x, viewport scrolling area width - viewport width)). - x = max(0.0f, min(x, scrolling_area.width() - viewport_width)); - // -> If the viewport has leftward overflow direction - // Let x be min(0, max(x, viewport width - viewport scrolling area width)). - - // 8. FIXME: For now we always assume overflow direction is downward - // -> If the viewport has downward overflow direction - // Let y be max(0, min(y, viewport scrolling area height - viewport height)). - y = max(0.0f, min(y, scrolling_area.height() - viewport_height)); - // -> If the viewport has upward overflow direction - // Let y be min(0, max(y, viewport height - viewport scrolling area height)). + // 8. -> If the viewport has downward overflow direction + // Let y be max(0, min(y, viewport scrolling area height - viewport height)). + // -> If the viewport has upward overflow direction + // Let y be min(0, max(y, viewport height - viewport scrolling area height)). + if (overflow_directions.vertical_axis_is_positive) { + y = max(0.0f, min(y, scrolling_area.height() - viewport_height)); + } else { + y = min(0.0f, max(y, viewport_height - scrolling_area.height())); + } } // FIXME: 9. Let position be the scroll position the viewport would have by aligning the x-coordinate x of the viewport diff --git a/Libraries/LibWeb/Layout/ScrollableOverflow.cpp b/Libraries/LibWeb/Layout/ScrollableOverflow.cpp index 45fc342000e0..029e81a9b0a9 100644 --- a/Libraries/LibWeb/Layout/ScrollableOverflow.cpp +++ b/Libraries/LibWeb/Layout/ScrollableOverflow.cpp @@ -6,7 +6,6 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include #include #include #include @@ -32,11 +31,6 @@ ContainedBoxesMap collect_scrollable_overflow_contained_boxes(Node const& root, return contained_boxes_map; } -struct PhysicalOverflowDirections { - bool horizontal_axis_is_positive { true }; - bool vertical_axis_is_positive { true }; -}; - struct AxisDirection { bool is_horizontal { false }; bool is_reverse { false }; @@ -56,8 +50,12 @@ static bool node_is_in_focused_text_control(DOM::Node const& node) && shadow_root->host()->is_focused(); } -static PhysicalOverflowDirections physical_overflow_directions(Box const& box) +// https://drafts.csswg.org/cssom-view/#overflow-directions +PhysicalOverflowDirections physical_overflow_directions(Box const& box) { + // A scrolling box of a viewport or element has two overflow directions, which are the block-end and inline-end + // directions for that viewport or element. + AxisDirection inline_axis { .is_horizontal = inline_axis_is_horizontal(box.writing_mode()), .is_reverse = box.inline_axis_is_reverse(), @@ -67,30 +65,24 @@ static PhysicalOverflowDirections physical_overflow_directions(Box const& box) .is_reverse = box.block_axis_is_reverse(), }; - auto horizontal_and_vertical_axes = [&]() { - if (!box.display().is_flex_inside()) - return AK::Tuple { inline_axis.is_horizontal ? inline_axis : block_axis, inline_axis.is_horizontal ? block_axis : inline_axis }; - + if (box.display().is_flex_inside()) { auto is_row_layout = box.flex_direction() == CSS::FlexDirection::Row || box.flex_direction() == CSS::FlexDirection::RowReverse; + auto& main_axis = is_row_layout ? inline_axis : block_axis; + auto& cross_axis = is_row_layout ? block_axis : inline_axis; - auto main_axis = is_row_layout ? inline_axis : block_axis; if (box.flex_direction() == CSS::FlexDirection::RowReverse || box.flex_direction() == CSS::FlexDirection::ColumnReverse) { main_axis.is_reverse = !main_axis.is_reverse; } - auto cross_axis = is_row_layout ? block_axis : inline_axis; // AD-HOC: A legacy webkit box ignores `flex-wrap`, matching other engines. if (!box.display().is_webkit_box_inside() && box.flex_wrap() == CSS::FlexWrap::WrapReverse) cross_axis.is_reverse = !cross_axis.is_reverse; + } - return AK::Tuple { main_axis.is_horizontal ? main_axis : cross_axis, main_axis.is_horizontal ? cross_axis : main_axis }; - }; - - auto axes = horizontal_and_vertical_axes(); - auto horizontal_axis = axes.get<0>(); - auto vertical_axis = axes.get<1>(); + auto horizontal_axis = inline_axis.is_horizontal ? inline_axis : block_axis; + auto vertical_axis = inline_axis.is_horizontal ? block_axis : inline_axis; return { .horizontal_axis_is_positive = !horizontal_axis.is_reverse, .vertical_axis_is_positive = !vertical_axis.is_reverse, diff --git a/Libraries/LibWeb/Layout/ScrollableOverflow.h b/Libraries/LibWeb/Layout/ScrollableOverflow.h index 1197d1002a5d..47b3a66bfcd5 100644 --- a/Libraries/LibWeb/Layout/ScrollableOverflow.h +++ b/Libraries/LibWeb/Layout/ScrollableOverflow.h @@ -19,6 +19,13 @@ namespace Web::Layout { // Map from each containing block to the boxes it contains. using ContainedBoxesMap = HashMap>; +struct PhysicalOverflowDirections { + bool horizontal_axis_is_positive { true }; + bool vertical_axis_is_positive { true }; +}; + +[[nodiscard]] PhysicalOverflowDirections physical_overflow_directions(Box const&); + [[nodiscard]] ContainedBoxesMap collect_scrollable_overflow_contained_boxes(Node const& root, Function box_visitor = {}); // https://drafts.csswg.org/css-overflow-3/#scrollable-overflow-region diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index 223c6e00269d..cba67b79d7a5 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -1127,17 +1127,43 @@ Optional Paintable::absolute_containing_line_box_rect() const return lines[*m_containing_line_box_index].rect.translated(containing_block->absolute_position()); } -CSSPixelPoint Paintable::clamp_scroll_offset(CSSPixelPoint offset) const +CSSPixelPoint Paintable::minimum_scroll_offset() const { auto scrollable_overflow_rect = this->scrollable_overflow_rect(); if (!scrollable_overflow_rect.has_value()) - return offset; + return {}; - auto padding_rect = absolute_padding_box_rect(); - auto max_x_offset = max(scrollable_overflow_rect->width() - padding_rect.width(), 0); - auto max_y_offset = max(scrollable_overflow_rect->height() - padding_rect.height(), 0); + auto scrollport_rect = absolute_padding_box_rect(); + return { + min(scrollable_overflow_rect->left() - scrollport_rect.left(), CSSPixels(0)), + min(scrollable_overflow_rect->top() - scrollport_rect.top(), CSSPixels(0)), + }; +} + +CSSPixelPoint Paintable::maximum_scroll_offset() const +{ + auto scrollable_overflow_rect = this->scrollable_overflow_rect(); + if (!scrollable_overflow_rect.has_value()) + return {}; - return { clamp(offset.x(), 0, max_x_offset), clamp(offset.y(), 0, max_y_offset) }; + auto scrollport_rect = absolute_padding_box_rect(); + return { + max(scrollable_overflow_rect->right() - scrollport_rect.right(), CSSPixels(0)), + max(scrollable_overflow_rect->bottom() - scrollport_rect.bottom(), CSSPixels(0)), + }; +} + +CSSPixelPoint Paintable::clamp_scroll_offset(CSSPixelPoint offset) const +{ + if (!scrollable_overflow_rect().has_value()) + return offset; + + auto minimum_offset = minimum_scroll_offset(); + auto maximum_offset = maximum_scroll_offset(); + return { + clamp(offset.x(), minimum_offset.x(), maximum_offset.x()), + clamp(offset.y(), minimum_offset.y(), maximum_offset.y()), + }; } Paintable::ScrollHandled Paintable::set_scroll_offset(CSSPixelPoint offset) @@ -1147,8 +1173,7 @@ Paintable::ScrollHandled Paintable::set_scroll_offset(CSSPixelPoint offset) offset = clamp_scroll_offset(offset); - // FIXME: If there is horizontal and vertical scroll ignore only part of the new offset - if (offset.y() < 0 || scroll_offset() == offset) + if (scroll_offset() == offset) return ScrollHandled::No; if (is_viewport_paintable()) { diff --git a/Libraries/LibWeb/Painting/Paintable.h b/Libraries/LibWeb/Painting/Paintable.h index 0818593fd15e..3ebfc4bf2659 100644 --- a/Libraries/LibWeb/Painting/Paintable.h +++ b/Libraries/LibWeb/Painting/Paintable.h @@ -220,6 +220,8 @@ class WEB_API Paintable }; CSSPixelPoint scroll_offset() const; + CSSPixelPoint minimum_scroll_offset() const; + CSSPixelPoint maximum_scroll_offset() const; CSSPixelPoint clamp_scroll_offset(CSSPixelPoint) const; CSSPixelRect scroll_snapport_rect() const; CSSPixelRect scroll_snapport_rect(CSSPixelRect scrollport) const; diff --git a/Tests/LibWeb/Text/expected/input-scrolls-caret-width-into-view.txt b/Tests/LibWeb/Text/expected/input-scrolls-caret-width-into-view.txt index 39a8776115f8..ec5620453187 100644 --- a/Tests/LibWeb/Text/expected/input-scrolls-caret-width-into-view.txt +++ b/Tests/LibWeb/Text/expected/input-scrolls-caret-width-into-view.txt @@ -1,3 +1,3 @@ scrolls to caret: true rect=[18,8 1x16] color=rgb(0, 0, 0) -RTL scrolls to caret: true +RTL caret needs no scroll: true diff --git a/Tests/LibWeb/Text/expected/scrollable-overflow-webkit-box-wrap-reverse.txt b/Tests/LibWeb/Text/expected/scrollable-overflow-webkit-box-wrap-reverse.txt new file mode 100644 index 000000000000..fd999a40a3cd --- /dev/null +++ b/Tests/LibWeb/Text/expected/scrollable-overflow-webkit-box-wrap-reverse.txt @@ -0,0 +1,2 @@ +legacy-box: scroll height: 150, scroll top range: [0, 100] +flex: scroll height: 150, scroll top range: [-100, 0] diff --git a/Tests/LibWeb/Text/input/input-scrolls-caret-width-into-view.html b/Tests/LibWeb/Text/input/input-scrolls-caret-width-into-view.html index c1da4473796e..af76bcd651d7 100644 --- a/Tests/LibWeb/Text/input/input-scrolls-caret-width-into-view.html +++ b/Tests/LibWeb/Text/input/input-scrolls-caret-width-into-view.html @@ -26,6 +26,6 @@ rtlInput.focus(); internals.sendText(rtlInput, "x"); const rtlInnerText = internals.getShadowRoot(rtlInput).querySelector("div > div"); - println(`RTL scrolls to caret: ${rtlInnerText.scrollLeft > 0}`); + println(`RTL caret needs no scroll: ${rtlInnerText.scrollLeft === 0}`); }); diff --git a/Tests/LibWeb/Text/input/scrollable-overflow-webkit-box-wrap-reverse.html b/Tests/LibWeb/Text/input/scrollable-overflow-webkit-box-wrap-reverse.html new file mode 100644 index 000000000000..29d31961a4af --- /dev/null +++ b/Tests/LibWeb/Text/input/scrollable-overflow-webkit-box-wrap-reverse.html @@ -0,0 +1,39 @@ + + + +
+
+ From a0b36bdf416a335bfbebcbdf9adbb862c5bd9467 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 6 Aug 2026 04:23:11 +0100 Subject: [PATCH 2/6] LibWeb: Position scrollbar thumbs within the full scroll offset range Thumb geometry previously assumed the scroll offset range begins at zero. Measure thumb travel from the minimum scroll offset so that a container scrolled to a negative offset shows the thumb in the correct position. --- Libraries/LibWeb/Painting/Paintable.cpp | 8 +++++--- Libraries/LibWeb/Painting/Scrollbar.cpp | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index cba67b79d7a5..904ff1ba11dd 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -1657,15 +1657,17 @@ Optional Paintable::compute_scrollbar_data(ScrollDirec ScrollbarData scrollbar_data = { .gutter_rect = {}, .thumb_rect = scrollbar_rect.value(), .thumb_travel_to_scroll_ratio = 0 }; + if (scrollable_overflow_length > scrollport_size) + scrollbar_data.thumb_travel_to_scroll_ratio = (usable_scrollbar_length - thumb_length) / (scrollable_overflow_length - scrollport_size); + scrollbar_data.thumb_rect.set_primary_size_for_orientation(orientation, thumb_length); scrollbar_data.thumb_rect.set_secondary_size_for_orientation(orientation, thumb_thickness); - scrollbar_data.thumb_rect.translate_primary_offset_for_orientation(orientation, thumb_margin); + auto minimum_offset = minimum_scroll_offset().primary_offset_for_orientation(orientation); + scrollbar_data.thumb_rect.translate_primary_offset_for_orientation(orientation, thumb_margin - minimum_offset * scrollbar_data.thumb_travel_to_scroll_ratio); if (with_gutter || (!is_horizontal && is_chrome_mirrored())) scrollbar_data.thumb_rect.translate_secondary_offset_for_orientation(orientation, thumb_margin); if (with_gutter) scrollbar_data.gutter_rect = scrollbar_rect.value(); - if (scrollable_overflow_length > scrollport_size) - scrollbar_data.thumb_travel_to_scroll_ratio = (usable_scrollbar_length - thumb_length) / (scrollable_overflow_length - scrollport_size); if (scroll_state_snapshot) { auto own_offset = scroll_state_snapshot->device_offset_for_index(m_own_scroll_node_index); diff --git a/Libraries/LibWeb/Painting/Scrollbar.cpp b/Libraries/LibWeb/Painting/Scrollbar.cpp index cedf7ddb75f0..0e53573a00f7 100644 --- a/Libraries/LibWeb/Painting/Scrollbar.cpp +++ b/Libraries/LibWeb/Painting/Scrollbar.cpp @@ -143,7 +143,8 @@ bool Scrollbar::scroll_to_mouse_position(CSSPixelPoint position) auto scrollable_overflow_size = paintable_box->scrollable_overflow_rect()->primary_size_for_orientation(orientation); auto padding_size = paintable_box->absolute_padding_box_rect().primary_size_for_orientation(orientation); - auto scroll_position_in_pixels = CSSPixels::nearest_value_for(scroll_position * (scrollable_overflow_size - padding_size)); + auto minimum_scroll_offset = paintable_box->minimum_scroll_offset().primary_offset_for_orientation(orientation); + auto scroll_position_in_pixels = minimum_scroll_offset + CSSPixels::nearest_value_for(scroll_position * (scrollable_overflow_size - padding_size)); auto new_scroll_offset = paintable_box->scroll_offset(); new_scroll_offset.set_primary_offset_for_orientation(orientation, scroll_position_in_pixels); From 8fbb95e008cee581fdfd43ba69f31332c5837c19 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 6 Aug 2026 04:30:14 +0100 Subject: [PATCH 3/6] LibWeb+Compositor: Clamp async scroll offsets to the minimum offset Async scroll nodes and viewport scrollbars only carried a maximum scroll offset. Record the minimum as well, so that wheel scrolling and scrollbar dragging on the compositor thread can reach negative offsets. --- .../LibWeb/Compositor/AsyncScrollTree.cpp | 8 +++---- .../LibWeb/Compositor/AsyncScrollingState.cpp | 2 ++ .../LibWeb/Compositor/AsyncScrollingState.h | 2 ++ .../LibWeb/Painting/DisplayListCommand.cpp | 8 +++---- .../LibWeb/Painting/DisplayListCommand.h | 2 ++ Libraries/LibWeb/Painting/Paintable.cpp | 21 +++++-------------- .../ViewportScrollbarController.cpp | 7 ++++--- .../blocking-wheel-event-regions.txt | 2 +- .../blocking-wheel-listener-invalidation.txt | 2 +- .../nested-navigable-wheel-admission.txt | 2 +- .../nested-scroller-keeps-sync-wheel.txt | 4 ++-- .../scroll-node-metadata-cache-replay.txt | 4 ++-- .../scroll-tree-parent-nodes.txt | 4 ++-- .../async-scrolling/scroll-tree-shape.txt | 4 ++-- .../expected/async-scrolling/sticky-areas.txt | 2 +- .../viewport-wheel-with-nested-scroller.txt | 4 ++-- .../wheel-scroll-admission.txt | 2 +- .../background-attachment-fixed.txt | 2 +- .../nested-overflow-containers.txt | 4 ++-- .../display_list/scrollable-in-fixed.txt | 2 +- .../sibling-scrollables-in-fixed.txt | 6 +++--- ...-foreignobject-scroller-after-relayout.txt | 2 +- 22 files changed, 46 insertions(+), 50 deletions(-) diff --git a/Libraries/LibWeb/Compositor/AsyncScrollTree.cpp b/Libraries/LibWeb/Compositor/AsyncScrollTree.cpp index 1854889684d0..559489af18be 100644 --- a/Libraries/LibWeb/Compositor/AsyncScrollTree.cpp +++ b/Libraries/LibWeb/Compositor/AsyncScrollTree.cpp @@ -66,8 +66,8 @@ AsyncStickyArea const* AsyncScrollTree::sticky_area_for_scroll_node_index(Painti Gfx::FloatPoint AsyncScrollTree::clamp_scroll_offset_to_node(AsyncScrollNode const& node, Gfx::FloatPoint scroll_offset) { - scroll_offset.set_x(max(0.0f, min(scroll_offset.x(), node.max_scroll_offset.x()))); - scroll_offset.set_y(max(0.0f, min(scroll_offset.y(), node.max_scroll_offset.y()))); + scroll_offset.set_x(max(node.min_scroll_offset.x(), min(scroll_offset.x(), node.max_scroll_offset.x()))); + scroll_offset.set_y(max(node.min_scroll_offset.y(), min(scroll_offset.y(), node.max_scroll_offset.y()))); return scroll_offset; } @@ -80,11 +80,11 @@ Gfx::FloatPoint AsyncScrollTree::scroll_offset_for_node(AsyncScrollNode const& n bool AsyncScrollTree::can_scroll_node_by_delta(AsyncScrollNode const& node, Painting::ScrollStateSnapshot const& scroll_state_snapshot, Gfx::FloatPoint delta) { auto scroll_offset = scroll_offset_for_node(node, scroll_state_snapshot); - if (node.can_be_wheel_scrolled_horizontally && delta.x() < 0 && scroll_offset.x() > 0) + if (node.can_be_wheel_scrolled_horizontally && delta.x() < 0 && scroll_offset.x() > node.min_scroll_offset.x()) return true; if (node.can_be_wheel_scrolled_horizontally && delta.x() > 0 && scroll_offset.x() < node.max_scroll_offset.x()) return true; - if (node.can_be_wheel_scrolled_vertically && delta.y() < 0 && scroll_offset.y() > 0) + if (node.can_be_wheel_scrolled_vertically && delta.y() < 0 && scroll_offset.y() > node.min_scroll_offset.y()) return true; if (node.can_be_wheel_scrolled_vertically && delta.y() > 0 && scroll_offset.y() < node.max_scroll_offset.y()) return true; diff --git a/Libraries/LibWeb/Compositor/AsyncScrollingState.cpp b/Libraries/LibWeb/Compositor/AsyncScrollingState.cpp index 0dcd9cd14065..62d1533a703b 100644 --- a/Libraries/LibWeb/Compositor/AsyncScrollingState.cpp +++ b/Libraries/LibWeb/Compositor/AsyncScrollingState.cpp @@ -101,6 +101,7 @@ AsyncScrollingState async_scrolling_state_from_display_list(Painting::DisplayLis .stable_node_id = stable_scroll_node_id_for(command.scrollable_node_id, command.scroll_node_kind, command.pseudo_element_type), .parent_node_id = {}, .scrollport_rect = command.scrollport_rect, + .min_scroll_offset = command.min_scroll_offset, .max_scroll_offset = command.max_scroll_offset, .is_viewport = command.is_viewport, .can_be_wheel_scrolled_horizontally = command.can_be_wheel_scrolled_horizontally, @@ -138,6 +139,7 @@ AsyncScrollingState async_scrolling_state_from_display_list(Painting::DisplayLis .expanded_thumb_rect = command.expanded_thumb_rect, .scroll_size = command.scroll_size, .expanded_scroll_size = command.expanded_scroll_size, + .min_scroll_offset = command.min_scroll_offset, .max_scroll_offset = command.max_scroll_offset, .thumb_color = command.thumb_color, .track_color = command.track_color, diff --git a/Libraries/LibWeb/Compositor/AsyncScrollingState.h b/Libraries/LibWeb/Compositor/AsyncScrollingState.h index 5c7f9489a545..70b0c66e4edc 100644 --- a/Libraries/LibWeb/Compositor/AsyncScrollingState.h +++ b/Libraries/LibWeb/Compositor/AsyncScrollingState.h @@ -59,6 +59,7 @@ struct AsyncScrollNode { AsyncScrollNodeStableID stable_node_id; Optional parent_node_id; Gfx::IntRect scrollport_rect; + Gfx::FloatPoint min_scroll_offset; Gfx::FloatPoint max_scroll_offset; bool is_viewport { false }; bool can_be_wheel_scrolled_horizontally { false }; @@ -111,6 +112,7 @@ struct ViewportScrollbar { Gfx::IntRect expanded_thumb_rect; double scroll_size { 0 }; double expanded_scroll_size { 0 }; + float min_scroll_offset { 0 }; float max_scroll_offset { 0 }; Color thumb_color; Color track_color; diff --git a/Libraries/LibWeb/Painting/DisplayListCommand.cpp b/Libraries/LibWeb/Painting/DisplayListCommand.cpp index e1bee6408341..ca80685df5fe 100644 --- a/Libraries/LibWeb/Painting/DisplayListCommand.cpp +++ b/Libraries/LibWeb/Painting/DisplayListCommand.cpp @@ -214,8 +214,8 @@ void PaintNestedDisplayList::dump(StringBuilder& builder) const void CompositorScrollNode::dump(StringBuilder& builder) const { - builder.appendff(" scroll_node_index={} parent_scroll_node_index={} scrollport_rect={} max_scroll_offset={} is_viewport={}", - scroll_node_index, parent_scroll_node_index, scrollport_rect, max_scroll_offset, is_viewport); + builder.appendff(" scroll_node_index={} parent_scroll_node_index={} scrollport_rect={} min_scroll_offset={} max_scroll_offset={} is_viewport={}", + scroll_node_index, parent_scroll_node_index, scrollport_rect, min_scroll_offset, max_scroll_offset, is_viewport); } static void dump_optional_float(StringBuilder& builder, Optional value) @@ -268,8 +268,8 @@ void CompositorMainThreadWheelEventRegion::dump(StringBuilder& builder) const void CompositorViewportScrollbar::dump(StringBuilder& builder) const { - builder.appendff(" scroll_node_index={} gutter_rect={} thumb_rect={} expanded_gutter_rect={} expanded_thumb_rect={} scroll_size={} expanded_scroll_size={} max_scroll_offset={} thumb_color={} track_color={} vertical={}", - scroll_node_index, gutter_rect, thumb_rect, expanded_gutter_rect, expanded_thumb_rect, scroll_size, expanded_scroll_size, max_scroll_offset, thumb_color, track_color, vertical); + builder.appendff(" scroll_node_index={} gutter_rect={} thumb_rect={} expanded_gutter_rect={} expanded_thumb_rect={} scroll_size={} expanded_scroll_size={} min_scroll_offset={} max_scroll_offset={} thumb_color={} track_color={} vertical={}", + scroll_node_index, gutter_rect, thumb_rect, expanded_gutter_rect, expanded_thumb_rect, scroll_size, expanded_scroll_size, min_scroll_offset, max_scroll_offset, thumb_color, track_color, vertical); } void PaintScrollBar::dump(StringBuilder&) const diff --git a/Libraries/LibWeb/Painting/DisplayListCommand.h b/Libraries/LibWeb/Painting/DisplayListCommand.h index 655bcb683998..9717ed3db6ce 100644 --- a/Libraries/LibWeb/Painting/DisplayListCommand.h +++ b/Libraries/LibWeb/Painting/DisplayListCommand.h @@ -599,6 +599,7 @@ struct CompositorScrollNode { VisualContextIndex scroll_node_index; VisualContextIndex parent_scroll_node_index; Gfx::IntRect scrollport_rect; + Gfx::FloatPoint min_scroll_offset; Gfx::FloatPoint max_scroll_offset; CompositorScrollNodeKind scroll_node_kind { CompositorScrollNodeKind::Element }; u8 pseudo_element_type { 0 }; @@ -683,6 +684,7 @@ struct CompositorViewportScrollbar { Gfx::IntRect expanded_thumb_rect; double scroll_size { 0 }; double expanded_scroll_size { 0 }; + float min_scroll_offset { 0 }; float max_scroll_offset { 0 }; Color thumb_color; Color track_color; diff --git a/Libraries/LibWeb/Painting/Paintable.cpp b/Libraries/LibWeb/Painting/Paintable.cpp index 904ff1ba11dd..dc0130c23ef3 100644 --- a/Libraries/LibWeb/Painting/Paintable.cpp +++ b/Libraries/LibWeb/Painting/Paintable.cpp @@ -632,20 +632,6 @@ static bool is_nested_navigable_container(Paintable const& paintable_box) return node && node->is_navigable_container() && as(*node).content_navigable(); } -static CSSPixelPoint maximum_scroll_offset_for(Paintable const& paintable_box) -{ - CSSPixelPoint max_scroll_offset; - auto scrollable_overflow_rect = paintable_box.scrollable_overflow_rect(); - if (!scrollable_overflow_rect.has_value()) - return max_scroll_offset; - - auto scrollport_rect = paintable_box.absolute_padding_box_rect(); - - max_scroll_offset.set_x(max(CSSPixels(0), scrollable_overflow_rect->width() - scrollport_rect.width())); - max_scroll_offset.set_y(max(CSSPixels(0), scrollable_overflow_rect->height() - scrollport_rect.height())); - return max_scroll_offset; -} - static void record_scroll_node(Paintable const& paintable_box, DisplayListRecordingContext& context) { auto scroll_node_kind = scroll_node_kind_for(paintable_box); @@ -667,7 +653,8 @@ static void record_scroll_node(Paintable const& paintable_box, DisplayListRecord .scroll_node_index = paintable_box.own_scroll_node_index(), .parent_scroll_node_index = parent_scroll_node_index, .scrollport_rect = scrollport_rect, - .max_scroll_offset = css_point_to_device_point(maximum_scroll_offset_for(paintable_box), context.device_pixels_per_css_pixel()), + .min_scroll_offset = css_point_to_device_point(paintable_box.minimum_scroll_offset(), context.device_pixels_per_css_pixel()), + .max_scroll_offset = css_point_to_device_point(paintable_box.maximum_scroll_offset(), context.device_pixels_per_css_pixel()), .scroll_node_kind = *scroll_node_kind, .pseudo_element_type = pseudo_element_type_for(paintable_box), .is_viewport = paintable_box.is_viewport_paintable(), @@ -811,7 +798,8 @@ static void record_viewport_scrollbar_state(Paintable const& paintable_box, Disp VERIFY(expanded_scrollbar_data.has_value()); auto gutter_rect = context.rounded_device_rect(scrollbar_data->gutter_rect).to_type(); - auto max_scroll_offset = css_point_to_device_point(maximum_scroll_offset_for(paintable_box), context.device_pixels_per_css_pixel()); + auto min_scroll_offset = css_point_to_device_point(paintable_box.minimum_scroll_offset(), context.device_pixels_per_css_pixel()); + auto max_scroll_offset = css_point_to_device_point(paintable_box.maximum_scroll_offset(), context.device_pixels_per_css_pixel()); auto orientation = direction == Paintable::ScrollDirection::Horizontal ? Gfx::Orientation::Horizontal : Gfx::Orientation::Vertical; context.display_list_recorder().compositor_viewport_scrollbar({ @@ -823,6 +811,7 @@ static void record_viewport_scrollbar_state(Paintable const& paintable_box, Disp .expanded_thumb_rect = context.rounded_device_rect(expanded_scrollbar_data->thumb_rect).to_type(), .scroll_size = scrollbar_data->thumb_travel_to_scroll_ratio.to_double(), .expanded_scroll_size = expanded_scrollbar_data->thumb_travel_to_scroll_ratio.to_double(), + .min_scroll_offset = min_scroll_offset.primary_offset_for_orientation(orientation), .max_scroll_offset = max_scroll_offset.primary_offset_for_orientation(orientation), .thumb_color = scrollbar_colors.thumb_color, .track_color = scrollbar_colors.track_color, diff --git a/Services/Compositor/ViewportScrollbarController.cpp b/Services/Compositor/ViewportScrollbarController.cpp index 77327292ea0f..80454734ebd8 100644 --- a/Services/Compositor/ViewportScrollbarController.cpp +++ b/Services/Compositor/ViewportScrollbarController.cpp @@ -200,10 +200,11 @@ Optional ViewportScrollbarController:: auto orientation = orientation_for_scrollbar(scrollbar); auto thumb_rect = expanded ? scrollbar.expanded_thumb_rect : scrollbar.thumb_rect; - auto min_thumb_position = static_cast(thumb_rect.primary_offset_for_orientation(orientation)); - auto max_thumb_position = min_thumb_position + scrollbar.max_scroll_offset * static_cast(scroll_size); + auto zero_offset_thumb_position = static_cast(thumb_rect.primary_offset_for_orientation(orientation)); + auto min_thumb_position = zero_offset_thumb_position + scrollbar.min_scroll_offset * static_cast(scroll_size); + auto max_thumb_position = zero_offset_thumb_position + scrollbar.max_scroll_offset * static_cast(scroll_size); auto target_thumb_position = AK::clamp(drag.primary_position - drag.thumb_grab_position, min_thumb_position, max_thumb_position); - auto target_scroll_offset = (target_thumb_position - min_thumb_position) / static_cast(scroll_size); + auto target_scroll_offset = (target_thumb_position - zero_offset_thumb_position) / static_cast(scroll_size); Gfx::FloatPoint delta; delta.set_primary_offset_for_orientation(orientation, target_scroll_offset - current_scroll_offset->primary_offset_for_orientation(orientation)); diff --git a/Tests/LibWeb/Text/expected/async-scrolling/blocking-wheel-event-regions.txt b/Tests/LibWeb/Text/expected/async-scrolling/blocking-wheel-event-regions.txt index ca44fcfb9790..e46abbdaf563 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/blocking-wheel-event-regions.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/blocking-wheel-event-regions.txt @@ -35,7 +35,7 @@ SaveLayer@0 CompositorWheelHitTestTarget@1 target_scroll_node_index=0 rect=[0,0 800x213] CompositorWheelHitTestTarget@1 target_scroll_node_index=0 rect=[0,0 800x200] CompositorWheelHitTestTarget@1 target_scroll_node_index=3 rect=[0,0 200x200] - CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[0,0 200x200] max_scroll_offset=[800,900] is_viewport=false + CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[0,0 200x200] min_scroll_offset=[0,0] max_scroll_offset=[800,900] is_viewport=false CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[0,0 100x100] CompositorBlockingWheelEventRegion@3 rect=[0,0 100x100] CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[0,100 1000x1000] diff --git a/Tests/LibWeb/Text/expected/async-scrolling/blocking-wheel-listener-invalidation.txt b/Tests/LibWeb/Text/expected/async-scrolling/blocking-wheel-listener-invalidation.txt index 2dfef90d733b..a535289f7b09 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/blocking-wheel-listener-invalidation.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/blocking-wheel-listener-invalidation.txt @@ -9,7 +9,7 @@ AccumulatedVisualContext Tree: DisplayList: SaveLayer@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=1 rect=[0,0 800x600] - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,1413] is_viewport=true + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,1413] is_viewport=true CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2013] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2000] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2000] diff --git a/Tests/LibWeb/Text/expected/async-scrolling/nested-navigable-wheel-admission.txt b/Tests/LibWeb/Text/expected/async-scrolling/nested-navigable-wheel-admission.txt index ce43cec41cf5..29d70ab38641 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/nested-navigable-wheel-admission.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/nested-navigable-wheel-admission.txt @@ -9,7 +9,7 @@ AccumulatedVisualContext Tree: DisplayList: SaveLayer@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=1 rect=[0,0 800x600] - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,1413] is_viewport=true + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,1413] is_viewport=true CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2013] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2000] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2000] diff --git a/Tests/LibWeb/Text/expected/async-scrolling/nested-scroller-keeps-sync-wheel.txt b/Tests/LibWeb/Text/expected/async-scrolling/nested-scroller-keeps-sync-wheel.txt index d16e0e7505d4..e800e5f28244 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/nested-scroller-keeps-sync-wheel.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/nested-scroller-keeps-sync-wheel.txt @@ -10,11 +10,11 @@ AccumulatedVisualContext Tree: DisplayList: SaveLayer@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=1 rect=[0,0 800x600] - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,1513] is_viewport=true + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,1513] is_viewport=true CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2113] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2100] CompositorWheelHitTestTarget@1 target_scroll_node_index=3 rect=[0,0 100x100] - CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] max_scroll_offset=[0,400] is_viewport=false + CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] min_scroll_offset=[0,0] max_scroll_offset=[0,400] is_viewport=false CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[0,0 100x500] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,100 800x2000] PaintScrollBar@1 diff --git a/Tests/LibWeb/Text/expected/async-scrolling/scroll-node-metadata-cache-replay.txt b/Tests/LibWeb/Text/expected/async-scrolling/scroll-node-metadata-cache-replay.txt index d45c0a53898d..facae3face7b 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/scroll-node-metadata-cache-replay.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/scroll-node-metadata-cache-replay.txt @@ -3,5 +3,5 @@ scroller scrollTop: 80 wheel target near viewport top: viewport upward wheel target over scroller: non-viewport scroll metadata: - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,2491] is_viewport=true - CompositorScrollNode@0 scroll_node_index=3 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] max_scroll_offset=[0,400] is_viewport=false + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,2491] is_viewport=true + CompositorScrollNode@0 scroll_node_index=3 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] min_scroll_offset=[0,0] max_scroll_offset=[0,400] is_viewport=false diff --git a/Tests/LibWeb/Text/expected/async-scrolling/scroll-tree-parent-nodes.txt b/Tests/LibWeb/Text/expected/async-scrolling/scroll-tree-parent-nodes.txt index 1caf2d635f28..4e7d282c8b64 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/scroll-tree-parent-nodes.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/scroll-tree-parent-nodes.txt @@ -13,14 +13,14 @@ AccumulatedVisualContext Tree: DisplayList: SaveLayer@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=1 rect=[0,0 800x600] - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,1513] is_viewport=true + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,1513] is_viewport=true CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2113] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2100] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,100 800x2000] CompositorWheelHitTestTarget@2 target_scroll_node_index=1 rect=[0,0 800x100] CompositorStickyArea@2 scroll_node_index=2 parent_scroll_node_index=1 nearest_scrolling_ancestor_index=1 position_relative_to_scroll_ancestor=[0,0] border_box_size=[800x100] scrollport_size=[800x600] containing_block_region=[0,0 800x2100] needs_parent_offset_adjustment=true inset_top=0 inset_right=none inset_bottom=none inset_left=none CompositorWheelHitTestTarget@2 target_scroll_node_index=4 rect=[0,0 100x100] - CompositorScrollNode@2 scroll_node_index=4 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] max_scroll_offset=[400,400] is_viewport=false + CompositorScrollNode@2 scroll_node_index=4 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] min_scroll_offset=[0,0] max_scroll_offset=[400,400] is_viewport=false CompositorWheelHitTestTarget@4 target_scroll_node_index=4 rect=[0,0 500x500] PaintScrollBar@2 PaintScrollBar@2 diff --git a/Tests/LibWeb/Text/expected/async-scrolling/scroll-tree-shape.txt b/Tests/LibWeb/Text/expected/async-scrolling/scroll-tree-shape.txt index 59e6b5aec6fe..ae6c35a4970d 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/scroll-tree-shape.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/scroll-tree-shape.txt @@ -15,11 +15,11 @@ AccumulatedVisualContext Tree: DisplayList: SaveLayer@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=1 rect=[0,0 800x600] - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,1513] is_viewport=true + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,1513] is_viewport=true CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2113] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2100] CompositorWheelHitTestTarget@1 target_scroll_node_index=3 rect=[0,0 100x100] - CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] max_scroll_offset=[400,400] is_viewport=false + CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] min_scroll_offset=[0,0] max_scroll_offset=[400,400] is_viewport=false CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[0,0 500x500] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,100 800x2000] PaintScrollBar@1 diff --git a/Tests/LibWeb/Text/expected/async-scrolling/sticky-areas.txt b/Tests/LibWeb/Text/expected/async-scrolling/sticky-areas.txt index 9322a3cd1630..5daf814a64e9 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/sticky-areas.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/sticky-areas.txt @@ -16,7 +16,7 @@ AccumulatedVisualContext Tree: DisplayList: SaveLayer@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=1 rect=[0,0 800x600] - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,1429] is_viewport=true + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,1429] is_viewport=true CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2029] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2016] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,16 800x2000] diff --git a/Tests/LibWeb/Text/expected/async-scrolling/viewport-wheel-with-nested-scroller.txt b/Tests/LibWeb/Text/expected/async-scrolling/viewport-wheel-with-nested-scroller.txt index 5b3374d0baad..0b36e19f49e5 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/viewport-wheel-with-nested-scroller.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/viewport-wheel-with-nested-scroller.txt @@ -12,11 +12,11 @@ AccumulatedVisualContext Tree: DisplayList: SaveLayer@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=1 rect=[0,0 800x600] - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,1513] is_viewport=true + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,1513] is_viewport=true CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2113] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x2100] CompositorWheelHitTestTarget@1 target_scroll_node_index=3 rect=[0,0 100x100] - CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] max_scroll_offset=[0,400] is_viewport=false + CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=1 scrollport_rect=[0,0 100x100] min_scroll_offset=[0,0] max_scroll_offset=[0,400] is_viewport=false CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[0,0 100x500] CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,100 800x2000] PaintScrollBar@1 diff --git a/Tests/LibWeb/Text/expected/async-scrolling/wheel-scroll-admission.txt b/Tests/LibWeb/Text/expected/async-scrolling/wheel-scroll-admission.txt index 590c36c76b46..5d0bc595433e 100644 --- a/Tests/LibWeb/Text/expected/async-scrolling/wheel-scroll-admission.txt +++ b/Tests/LibWeb/Text/expected/async-scrolling/wheel-scroll-admission.txt @@ -22,7 +22,7 @@ SaveLayer@0 CompositorWheelHitTestTarget@1 target_scroll_node_index=0 rect=[0,0 800x213] CompositorWheelHitTestTarget@1 target_scroll_node_index=0 rect=[0,0 800x200] CompositorWheelHitTestTarget@1 target_scroll_node_index=3 rect=[0,0 200x200] - CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[0,0 200x200] max_scroll_offset=[800,900] is_viewport=false + CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[0,0 200x200] min_scroll_offset=[0,0] max_scroll_offset=[800,900] is_viewport=false CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[0,0 100x100] CompositorBlockingWheelEventRegion@3 rect=[0,0 100x100] CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[0,100 1000x1000] diff --git a/Tests/LibWeb/Text/expected/display_list/background-attachment-fixed.txt b/Tests/LibWeb/Text/expected/display_list/background-attachment-fixed.txt index 597092e08fb0..0be6a75bacae 100644 --- a/Tests/LibWeb/Text/expected/display_list/background-attachment-fixed.txt +++ b/Tests/LibWeb/Text/expected/display_list/background-attachment-fixed.txt @@ -6,7 +6,7 @@ AccumulatedVisualContext Tree: DisplayList: SaveLayer@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=1 rect=[0,0 800x600] - CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] max_scroll_offset=[0,613] is_viewport=true + CompositorScrollNode@0 scroll_node_index=1 parent_scroll_node_index=0 scrollport_rect=[0,0 800x600] min_scroll_offset=[0,0] max_scroll_offset=[0,613] is_viewport=true CompositorWheelHitTestTarget@1 target_scroll_node_index=1 rect=[0,0 800x1213] Save@1 PaintLinearGradient@2 rect=[0,0 800x600] diff --git a/Tests/LibWeb/Text/expected/display_list/nested-overflow-containers.txt b/Tests/LibWeb/Text/expected/display_list/nested-overflow-containers.txt index 3aa7ca6f35ed..b8f467c22b53 100644 --- a/Tests/LibWeb/Text/expected/display_list/nested-overflow-containers.txt +++ b/Tests/LibWeb/Text/expected/display_list/nested-overflow-containers.txt @@ -12,10 +12,10 @@ SaveLayer@0 CompositorWheelHitTestTarget@1 target_scroll_node_index=0 rect=[0,0 800x175] CompositorWheelHitTestTarget@1 target_scroll_node_index=0 rect=[8,8 784x154] CompositorWheelHitTestTarget@1 target_scroll_node_index=3 rect=[8,8 204x154] - CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[10,10 200x150] max_scroll_offset=[102,0] is_viewport=false + CompositorScrollNode@1 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[10,10 200x150] min_scroll_offset=[0,0] max_scroll_offset=[102,0] is_viewport=false FillPath@1 path_bounding_rect=[8,8 204x154] CompositorWheelHitTestTarget@3 target_scroll_node_index=5 rect=[10,10 302x102] - CompositorScrollNode@3 scroll_node_index=5 parent_scroll_node_index=3 scrollport_rect=[11,11 300x100] max_scroll_offset=[100,0] is_viewport=false + CompositorScrollNode@3 scroll_node_index=5 parent_scroll_node_index=3 scrollport_rect=[11,11 300x100] min_scroll_offset=[0,0] max_scroll_offset=[100,0] is_viewport=false FillRect@3 rect=[10,10 302x102] color=rgb(255, 255, 224) FillPath@3 path_bounding_rect=[10,10 302x102] CompositorWheelHitTestTarget@5 target_scroll_node_index=5 rect=[11,11 400x80] diff --git a/Tests/LibWeb/Text/expected/display_list/scrollable-in-fixed.txt b/Tests/LibWeb/Text/expected/display_list/scrollable-in-fixed.txt index 4fd2b1a8bb7f..1980ecd9b614 100644 --- a/Tests/LibWeb/Text/expected/display_list/scrollable-in-fixed.txt +++ b/Tests/LibWeb/Text/expected/display_list/scrollable-in-fixed.txt @@ -12,7 +12,7 @@ SaveLayer@0 FillRect@0 rect=[20,20 204x154] color=rgb(211, 211, 211) FillPath@0 path_bounding_rect=[20,20 204x154] CompositorWheelHitTestTarget@0 target_scroll_node_index=3 rect=[32,32 182x102] - CompositorScrollNode@0 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[33,33 180x100] max_scroll_offset=[120,100] is_viewport=false + CompositorScrollNode@0 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[33,33 180x100] min_scroll_offset=[0,0] max_scroll_offset=[120,100] is_viewport=false FillPath@0 path_bounding_rect=[32,32 182x102] CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[33,33 300x200] FillRect@3 rect=[33,33 300x200] color=rgb(240, 128, 128) diff --git a/Tests/LibWeb/Text/expected/display_list/sibling-scrollables-in-fixed.txt b/Tests/LibWeb/Text/expected/display_list/sibling-scrollables-in-fixed.txt index 1b8795793b1e..2ec1d349d026 100644 --- a/Tests/LibWeb/Text/expected/display_list/sibling-scrollables-in-fixed.txt +++ b/Tests/LibWeb/Text/expected/display_list/sibling-scrollables-in-fixed.txt @@ -16,7 +16,7 @@ SaveLayer@0 FillRect@0 rect=[10,10 324x224] color=rgb(211, 211, 211) FillPath@0 path_bounding_rect=[10,10 324x224] CompositorWheelHitTestTarget@0 target_scroll_node_index=3 rect=[22,22 93.328125x152] - CompositorScrollNode@0 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[23,23 91x150] max_scroll_offset=[108.671875,150] is_viewport=false + CompositorScrollNode@0 scroll_node_index=3 parent_scroll_node_index=0 scrollport_rect=[23,23 91x150] min_scroll_offset=[0,0] max_scroll_offset=[108.671875,150] is_viewport=false FillPath@0 path_bounding_rect=[22,22 93x152] CompositorWheelHitTestTarget@3 target_scroll_node_index=3 rect=[23,23 200x300] FillRect@3 rect=[23,23 200x300] color=rgb(240, 128, 128) @@ -24,7 +24,7 @@ SaveLayer@0 PaintScrollBar@0 PaintScrollBar@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=5 rect=[125.328125,22 93.328125x152] - CompositorScrollNode@0 scroll_node_index=5 parent_scroll_node_index=0 scrollport_rect=[126,23 92x150] max_scroll_offset=[108.671875,150] is_viewport=false + CompositorScrollNode@0 scroll_node_index=5 parent_scroll_node_index=0 scrollport_rect=[126,23 92x150] min_scroll_offset=[0,0] max_scroll_offset=[108.671875,150] is_viewport=false FillPath@0 path_bounding_rect=[125,22 94x152] CompositorWheelHitTestTarget@5 target_scroll_node_index=5 rect=[126.328125,23 200x300] FillRect@5 rect=[126,23 200x300] color=rgb(144, 238, 144) @@ -32,7 +32,7 @@ SaveLayer@0 PaintScrollBar@0 PaintScrollBar@0 CompositorWheelHitTestTarget@0 target_scroll_node_index=7 rect=[228.65625,22 93.328125x152] - CompositorScrollNode@0 scroll_node_index=7 parent_scroll_node_index=0 scrollport_rect=[230,23 91x150] max_scroll_offset=[108.671875,150] is_viewport=false + CompositorScrollNode@0 scroll_node_index=7 parent_scroll_node_index=0 scrollport_rect=[230,23 91x150] min_scroll_offset=[0,0] max_scroll_offset=[108.671875,150] is_viewport=false FillPath@0 path_bounding_rect=[229,22 93x152] CompositorWheelHitTestTarget@7 target_scroll_node_index=7 rect=[229.65625,23 200x300] FillRect@7 rect=[230,23 200x300] color=rgb(173, 216, 230) diff --git a/Tests/LibWeb/Text/expected/display_list/svg-foreignobject-scroller-after-relayout.txt b/Tests/LibWeb/Text/expected/display_list/svg-foreignobject-scroller-after-relayout.txt index c29acfd4c224..3c6abcc9b7ca 100644 --- a/Tests/LibWeb/Text/expected/display_list/svg-foreignobject-scroller-after-relayout.txt +++ b/Tests/LibWeb/Text/expected/display_list/svg-foreignobject-scroller-after-relayout.txt @@ -16,7 +16,7 @@ SaveLayer@0 StrokePath@2 path_bounding_rect=[17,17 82x82] thickness=1 color=rgb(0, 0, 0) CompositorWheelHitTestTarget@2 target_scroll_node_index=0 rect=[8,8 100x100] CompositorWheelHitTestTarget@3 target_scroll_node_index=5 rect=[8,8 80x80] - CompositorScrollNode@3 scroll_node_index=5 parent_scroll_node_index=0 scrollport_rect=[8,8 80x80] max_scroll_offset=[0,220] is_viewport=false + CompositorScrollNode@3 scroll_node_index=5 parent_scroll_node_index=0 scrollport_rect=[8,8 80x80] min_scroll_offset=[0,0] max_scroll_offset=[0,220] is_viewport=false CompositorWheelHitTestTarget@5 target_scroll_node_index=5 rect=[8,8 80x300] FillRect@5 rect=[8,8 80x300] color=rgb(0, 0, 0) PaintScrollBar@3 From b09024954539b25e4a472ca29501e06523261c95 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 6 Aug 2026 04:46:29 +0100 Subject: [PATCH 4/6] LibWeb: Honor `direction` when resolving over-constrained block margins CSS 2 assigns leftover inline space to the end-side margin, which is the left margin when the containing block's direction is `rtl`. Block-level boxes in right-to-left containers are now aligned to the right, and boxes wider than their container overflow to the left. --- .../src/layout/block_formatting_context.rs | 7 +- ...spos-after-rtl-inline-static-position.html | 1 + .../css-overflow/overflow-rtl-scroll-left.txt | 6 + .../css/cssom-view/scrollLeftTop.txt | 12 ++ .../overflow-rtl-scroll-left.html | 49 +++++++ .../css/cssom-view/scrollLeftTop.html | 137 ++++++++++++++++++ 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-overflow/overflow-rtl-scroll-left.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/cssom-view/scrollLeftTop.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-overflow/overflow-rtl-scroll-left.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/cssom-view/scrollLeftTop.html diff --git a/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs b/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs index 7351c1e87d85..f3a53d65c576 100644 --- a/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs +++ b/Libraries/LibWeb/Rust/src/layout/block_formatting_context.rs @@ -575,6 +575,7 @@ impl BlockFormattingContext { } let remaining_inline_size = remaining_available_space.inline_size.to_px_or_zero(); + let containing_block_direction = self.style(self.containing_block(node)).direction(); let computed_margin_left = style.margin_left(); let computed_margin_right = style.margin_right(); let compute = |input: Option, @@ -636,7 +637,11 @@ impl BlockFormattingContext { unreachable!(); } } else if !*margin_left_is_auto && !*margin_right_is_auto { - *margin_right += underflow; + if containing_block_direction == direction::RTL { + *margin_left += underflow; + } else { + *margin_right += underflow; + } } else if !*margin_left_is_auto && *margin_right_is_auto { *margin_right = underflow; *margin_right_is_auto = false; diff --git a/Tests/LibWeb/Ref/input/abspos-after-rtl-inline-static-position.html b/Tests/LibWeb/Ref/input/abspos-after-rtl-inline-static-position.html index 3c6fc3864507..208f2dafa096 100644 --- a/Tests/LibWeb/Ref/input/abspos-after-rtl-inline-static-position.html +++ b/Tests/LibWeb/Ref/input/abspos-after-rtl-inline-static-position.html @@ -16,6 +16,7 @@ list-style: none; margin: 0; padding: 0; + width: 100px; } li { diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-overflow/overflow-rtl-scroll-left.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-overflow/overflow-rtl-scroll-left.txt new file mode 100644 index 000000000000..af152722ad46 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-overflow/overflow-rtl-scroll-left.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass rtl scroll left should be 0 when overflow size is empty \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/cssom-view/scrollLeftTop.txt b/Tests/LibWeb/Text/expected/wpt-import/css/cssom-view/scrollLeftTop.txt new file mode 100644 index 000000000000..1a0543ca8ec4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/cssom-view/scrollLeftTop.txt @@ -0,0 +1,12 @@ +Harness status: OK + +Found 6 tests + +3 Pass +3 Fail +Pass writing-mode:horizontal-tb; direction:ltr +Pass writing-mode:horizontal-tb; direction:rtl +Pass writing-mode:vertical-lr; direction:ltr +Fail writing-mode:vertical-lr; direction:rtl +Fail writing-mode:vertical-rl; direction:ltr +Fail writing-mode:vertical-rl; direction:rtl \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-overflow/overflow-rtl-scroll-left.html b/Tests/LibWeb/Text/input/wpt-import/css/css-overflow/overflow-rtl-scroll-left.html new file mode 100644 index 000000000000..1d517c77b376 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-overflow/overflow-rtl-scroll-left.html @@ -0,0 +1,49 @@ + + +overflow: rtl scroll left should return 0 when overflow size is empty + + + + + + + +
+
+
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/cssom-view/scrollLeftTop.html b/Tests/LibWeb/Text/input/wpt-import/css/cssom-view/scrollLeftTop.html new file mode 100644 index 000000000000..413ed6ef3350 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/cssom-view/scrollLeftTop.html @@ -0,0 +1,137 @@ + +CSSOM View - scrollLeft/scrollTop considers writing-mode and css direction + + + + + + + + + + + + + + + +

scrollLeft/scrollTop

+

writing-mode: horizontal-tb;

+
+
+
+
+
+
+

writing-mode: vertical-lr;

+
+
+
+
+
+
+

writing-mode: vertical-rl;

+
+
+
+
+
+
+ + + + From 8664f622c81f5572f6590e587af81014d1e51d8d Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 6 Aug 2026 04:52:34 +0100 Subject: [PATCH 5/6] LibWeb: Align overflowing line boxes to the inline start edge Previously, a line box wider than its container was kept flush with the left content edge. Content in a right-to-left container therefore overflowed to the right, where it is unreachable. We now ignore `text-align` for overflowing lines and align them to the inline start edge. In a right-to-left container the overflow extends past the left content edge, where it can be scrolled to. --- .../LibWeb/Rust/src/layout/line_builder.rs | 4 ++++ .../rtl-overflowing-line-inline-position.txt | 18 ++++++++++++++++++ .../rtl-overflowing-line-inline-position.html | 8 ++++++++ 3 files changed, 30 insertions(+) create mode 100644 Tests/LibWeb/Layout/expected/block-and-inline/rtl-overflowing-line-inline-position.txt create mode 100644 Tests/LibWeb/Layout/input/block-and-inline/rtl-overflowing-line-inline-position.html diff --git a/Libraries/LibWeb/Rust/src/layout/line_builder.rs b/Libraries/LibWeb/Rust/src/layout/line_builder.rs index fc35f02380af..22164eece411 100644 --- a/Libraries/LibWeb/Rust/src/layout/line_builder.rs +++ b/Libraries/LibWeb/Rust/src/layout/line_builder.rs @@ -620,6 +620,10 @@ impl<'builder, 'context> LineBuilder<'builder, 'context> { text_align::MATCH_PARENT => unreachable!("match-parent must be resolved"), _ => {} } + } else if excess < CssPixels::default() && containing_style.direction() == direction::RTL { + // An overflowing line ignores text-align and is aligned to the inline start edge. + // In a right-to-left container the overflow therefore extends past the line-left edge. + inline_offset += excess; } let strut_baseline = Self::baseline_for_style(containing_style, containing_style.line_height()); diff --git a/Tests/LibWeb/Layout/expected/block-and-inline/rtl-overflowing-line-inline-position.txt b/Tests/LibWeb/Layout/expected/block-and-inline/rtl-overflowing-line-inline-position.txt new file mode 100644 index 000000000000..09f2b78f3123 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/block-and-inline/rtl-overflowing-line-inline-position.txt @@ -0,0 +1,18 @@ +Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] [BFC] children: not-inline + BlockContainer at [0,0] [0+0+0 800 0+0+0] [0+0+0 32 0+0+0] [BFC] children: not-inline + BlockContainer at [8,8] [8+0+0 784 0+0+8] [8+0+0 16 0+0+8] children: not-inline + BlockContainer
at [8,8] [0+0+0 100 0+0+684] [0+0+0 16 0+0+0] children: inline + frag 0 from TextNode start: 0, length: 20, rect: [-136.0625,8 244.0625x16] baseline: 12.796875 + "mmmmmmmmmmmmmmmmmmmm" + TextNode <#text> (not painted) + BlockContainer <(anonymous)> at [8,24] [0+0+0 784 0+0+0] [0+0+0 0 0+0+0] children: inline + TextNode <#text> (not painted) + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x32] + PaintableWithLines (BlockContainer) [8,8 784x16] + PaintableWithLines (BlockContainer
) [8,8 100x16] + PaintableWithLines (BlockContainer(anonymous)) [8,24 784x0] + +SC for Viewport<#document> [0,0 800x600] (z-index: auto) + SC for BlockContainer [0,0 800x32] (z-index: auto) diff --git a/Tests/LibWeb/Layout/input/block-and-inline/rtl-overflowing-line-inline-position.html b/Tests/LibWeb/Layout/input/block-and-inline/rtl-overflowing-line-inline-position.html new file mode 100644 index 000000000000..2e1e580c6098 --- /dev/null +++ b/Tests/LibWeb/Layout/input/block-and-inline/rtl-overflowing-line-inline-position.html @@ -0,0 +1,8 @@ +
mmmmmmmmmmmmmmmmmmmm
From 81aacdd922982122508ed9bbae6a70616d8cf2bc Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Thu, 6 Aug 2026 15:41:08 +0100 Subject: [PATCH 6/6] LibWeb: Propagate the principal writing mode to the viewport The viewport's computed values previously kept the initial writing mode and direction, so the leftward overflow of a right-to-left document was clipped as unreachable and the viewport could not scroll to it. The viewport now takes the principal writing mode from the root element, or from its first body child in HTML documents. --- Libraries/LibWeb/DOM/Document.cpp | 45 +++++++++++++++++++ .../rtl-viewport-programmatic-scroll.txt | 2 + .../rtl-viewport-programmatic-scroll.html | 18 ++++++++ 3 files changed, 65 insertions(+) create mode 100644 Tests/LibWeb/Text/expected/rtl-viewport-programmatic-scroll.txt create mode 100644 Tests/LibWeb/Text/input/rtl-viewport-programmatic-scroll.html diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 6287ba057850..78d0da49fa42 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -1849,6 +1849,50 @@ static void propagate_scrollbar_width_to_viewport(Element& root_element, Layout: }); } +// https://drafts.csswg.org/css-writing-modes-4/#principal-flow +static void propagate_principal_writing_mode_to_viewport(Element& root_element, Layout::Viewport& viewport) +{ + // The principal writing mode of the document is determined by the used writing-mode, direction, and + // text-orientation values of the root element. + auto const* root_inherited_box_values = root_element.style_group(); + VERIFY(root_inherited_box_values); + auto writing_mode = static_cast(root_inherited_box_values->writing_mode); + auto direction = static_cast(root_inherited_box_values->direction); + + // As a special case for handling HTML documents, if the root element has a body child element [HTML] whose + // display value is not none, the used value of the of writing-mode and direction properties on root element + // are taken from the computed writing-mode and direction of the first such child element instead of from the + // root element's own values. + // NOTE: Using containment disables this special handling of the HTML body element. + auto* body_element = root_element.first_child_of_type(); + auto const* root_box_values = root_element.style_group(); + VERIFY(root_box_values); + auto const* body_box_values = body_element ? body_element->style_group() : nullptr; + auto const* body_inherited_box_values = body_element ? body_element->style_group() : nullptr; + auto has_containment = [](CSS::ComputedValues::BoxValues const& values) { + return values.size_containment || values.inline_size_containment || values.layout_containment || values.style_containment || values.paint_containment; + }; + bool propagation_is_disabled_by_containment = has_containment(*root_box_values) + || (body_box_values && has_containment(*body_box_values)); + if (root_element.is_html_html_element() && !propagation_is_disabled_by_containment + && body_box_values && body_inherited_box_values && !CSS::display_from_ffi_display(body_box_values->display).is_none()) { + writing_mode = static_cast(body_inherited_box_values->writing_mode); + direction = static_cast(body_inherited_box_values->direction); + } + root_element.unsafe_layout_node()->modify_computed_values([&](auto& values) { + values.set_writing_mode(writing_mode); + values.set_direction(direction); + }); + + // https://drafts.csswg.org/css-writing-modes-4/#icb + // The principal writing mode is propagated to the initial containing block and to the viewport, thereby + // affecting the layout of the root element and the scrolling direction of the viewport. + viewport.modify_computed_values([&](auto& values) { + values.set_writing_mode(writing_mode); + values.set_direction(direction); + }); +} + // https://drafts.csswg.org/css-overflow-3/#overflow-propagation static void propagate_overflow_to_viewport(Element& root_element, Layout::Viewport& viewport) { @@ -2161,6 +2205,7 @@ void Document::update_layout(UpdateLayoutReason reason) } if (document_element && document_element->unsafe_layout_node()) { + propagate_principal_writing_mode_to_viewport(*document_element, *m_layout_root); propagate_overflow_to_viewport(*document_element, *m_layout_root); } else { m_layout_root->modify_computed_values([](auto& values) { diff --git a/Tests/LibWeb/Text/expected/rtl-viewport-programmatic-scroll.txt b/Tests/LibWeb/Text/expected/rtl-viewport-programmatic-scroll.txt new file mode 100644 index 000000000000..965f0fb824e8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/rtl-viewport-programmatic-scroll.txt @@ -0,0 +1,2 @@ +root-rtl: scroll x range: [-300, 0] +body-rtl: scroll x range: [-300, 0] diff --git a/Tests/LibWeb/Text/input/rtl-viewport-programmatic-scroll.html b/Tests/LibWeb/Text/input/rtl-viewport-programmatic-scroll.html new file mode 100644 index 000000000000..bdb7fe308203 --- /dev/null +++ b/Tests/LibWeb/Text/input/rtl-viewport-programmatic-scroll.html @@ -0,0 +1,18 @@ + + + + +