LibWeb: Handle stale scrollbar data during capture

Page script can change overflow while a scrollbar owns chrome widget
capture. Treat missing scrollbar data as a normal condition instead of
verifying, and keep an active thumb grab captured until pointerup.

Add a regression test that hides a horizontal scrollbar on pointerdown
and then continues the drag.
This commit is contained in:
Andreas Kling 2026-05-16 22:41:30 +02:00 committed by Andreas Kling
parent d312c4f427
commit 410df3218d
4 changed files with 45 additions and 6 deletions

View file

@ -49,7 +49,8 @@ MouseAction Scrollbar::handle_pointer_event(FlyString const& type, unsigned butt
return MouseAction::None;
auto position = paintable_box->transform_to_local_coordinates(visual_viewport_position);
scroll_to_mouse_position(position);
if (!scroll_to_mouse_position(position) && !m_thumb_grab_position.has_value())
return MouseAction::None;
paintable_box->set_needs_repaint();
if (type == UIEvents::EventNames::pointerup) {
@ -99,17 +100,18 @@ void Scrollbar::mouse_leave()
paintable_box->set_needs_repaint();
}
void Scrollbar::scroll_to_mouse_position(CSSPixelPoint position)
bool Scrollbar::scroll_to_mouse_position(CSSPixelPoint position)
{
auto paintable_box = m_paintable_box.strong_ref();
if (!paintable_box)
return;
return false;
ChromeMetrics metrics = paintable_box->document().page().chrome_metrics();
auto const& scroll_state = paintable_box->document().paintable()->scroll_state_snapshot();
auto scrollbar_data = paintable_box->compute_scrollbar_data(m_direction, metrics, &scroll_state);
VERIFY(scrollbar_data.has_value());
if (!scrollbar_data.has_value())
return false;
auto orientation = m_direction == PaintableBox::ScrollDirection::Horizontal ? Orientation::Horizontal : Orientation::Vertical;
auto offset_relative_to_gutter = (position - scrollbar_data->gutter_rect.location()).primary_offset_for_orientation(orientation);
@ -117,7 +119,7 @@ void Scrollbar::scroll_to_mouse_position(CSSPixelPoint position)
auto thumb_size = scrollbar_data->thumb_rect.primary_size_for_orientation(orientation);
if (gutter_size < thumb_size)
return;
return true;
if (!m_thumb_grab_position.has_value()) {
m_thumb_grab_position = scrollbar_data->thumb_rect.contains(position)
@ -135,6 +137,7 @@ void 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);
paintable_box->set_scroll_offset(new_scroll_offset);
return true;
}
}

View file

@ -30,7 +30,7 @@ private:
MouseAction mouse_down(CSSPixelPoint, unsigned button);
MouseAction mouse_move(CSSPixelPoint);
MouseAction mouse_up(CSSPixelPoint, unsigned button);
void scroll_to_mouse_position(CSSPixelPoint);
bool scroll_to_mouse_position(CSSPixelPoint);
WeakPtr<PaintableBox> m_paintable_box;
PaintableBox::ScrollDirection m_direction;

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<style>
body {
margin: 0;
}
#scrollable {
width: 100px;
height: 100px;
overflow-x: scroll;
overflow-y: hidden;
}
#content {
width: 1000px;
height: 80px;
}
</style>
<div id="scrollable">
<div id="content"></div>
</div>
<script src="../include.js"></script>
<script>
test(() => {
scrollable.addEventListener("pointerdown", () => {
scrollable.style.overflowX = "hidden";
}, { once: true });
internals.mouseDown(50, 97);
internals.mouseMove(80, 97);
internals.mouseUp(80, 97);
println("PASS");
});
</script>