LibWeb: Limit scroll bar thumb movement based on grab point

We were calculating the scroll delta based on the last known mouse
position, even if that position ventured far beyond the scroll bar's
rect. This caused weird behavior such as scrolling when the mouse was
clearly not on the scrollbar.

This reworks the scrollbar logic to use the mouse's absolute position
instead of a delta, and to always calculate and set the absolute scroll
offset. This makes it much easier to reason about the scrollbar's
position, plus we don't need the last mouse position anymore.

As far as I can tell, our scroll bar behavior now closely resembles
Firefox' behavior.
This commit is contained in:
Jelle Raaijmakers 2025-06-18 12:00:21 +02:00 committed by Tim Flynn
commit e56146edec
Notes: github-actions[bot] 2025-06-19 11:04:59 +00:00
2 changed files with 40 additions and 35 deletions

View file

@ -916,24 +916,15 @@ Paintable::DispatchEventOfSameName PaintableBox::handle_mousedown(Badge<EventHan
position = adjust_position_for_cumulative_scroll_offset(position);
auto handle_scrollbar = [&](auto direction) {
auto scrollbar_data = compute_scrollbar_data(direction, AdjustThumbRectForScrollOffset::Yes);
auto scrollbar_data = compute_scrollbar_data(direction);
if (!scrollbar_data.has_value())
return false;
if (scrollbar_data->thumb_rect.contains(position)) {
m_last_mouse_tracking_position = position;
m_scroll_thumb_dragging_direction = direction;
navigable()->event_handler().set_mouse_event_tracking_paintable(this);
return true;
}
if (scrollbar_data->gutter_rect.contains(position)) {
m_last_mouse_tracking_position = scrollbar_data->thumb_rect.center();
m_scroll_thumb_dragging_direction = direction;
navigable()->event_handler().set_mouse_event_tracking_paintable(this);
scroll_to_mouse_postion(position);
scroll_to_mouse_position(position);
return true;
}
@ -950,10 +941,10 @@ Paintable::DispatchEventOfSameName PaintableBox::handle_mousedown(Badge<EventHan
Paintable::DispatchEventOfSameName PaintableBox::handle_mouseup(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned)
{
if (m_last_mouse_tracking_position.has_value()) {
m_last_mouse_tracking_position.clear();
if (m_scroll_thumb_grab_position.has_value()) {
m_scroll_thumb_grab_position.clear();
m_scroll_thumb_dragging_direction.clear();
const_cast<HTML::Navigable&>(*navigable()).event_handler().set_mouse_event_tracking_paintable(nullptr);
navigable()->event_handler().set_mouse_event_tracking_paintable(nullptr);
}
return Paintable::DispatchEventOfSameName::Yes;
}
@ -962,8 +953,8 @@ Paintable::DispatchEventOfSameName PaintableBox::handle_mousemove(Badge<EventHan
{
position = adjust_position_for_cumulative_scroll_offset(position);
if (m_last_mouse_tracking_position.has_value()) {
scroll_to_mouse_postion(position);
if (m_scroll_thumb_grab_position.has_value()) {
scroll_to_mouse_position(position);
return Paintable::DispatchEventOfSameName::No;
}
@ -995,30 +986,44 @@ bool PaintableBox::scrollbar_contains_mouse_position(ScrollDirection direction,
return scrollbar_data->gutter_rect.contains(position);
}
void PaintableBox::scroll_to_mouse_postion(CSSPixelPoint position)
void PaintableBox::scroll_to_mouse_position(CSSPixelPoint position)
{
VERIFY(m_last_mouse_tracking_position.has_value());
VERIFY(m_scroll_thumb_dragging_direction.has_value());
Gfx::Point<double> scroll_delta;
if (m_scroll_thumb_dragging_direction == ScrollDirection::Horizontal)
scroll_delta.set_x((position.x() - m_last_mouse_tracking_position->x()).to_double());
else
scroll_delta.set_y((position.y() - m_last_mouse_tracking_position->y()).to_double());
auto scrollbar_data = compute_scrollbar_data(m_scroll_thumb_dragging_direction.value(), AdjustThumbRectForScrollOffset::Yes);
VERIFY(scrollbar_data.has_value());
auto padding_rect = absolute_padding_box_rect();
auto scrollable_overflow_rect = this->scrollable_overflow_rect().value();
auto scroll_overflow_size = m_scroll_thumb_dragging_direction == ScrollDirection::Horizontal ? scrollable_overflow_rect.width() : scrollable_overflow_rect.height();
auto scrollport_size = m_scroll_thumb_dragging_direction == ScrollDirection::Horizontal ? padding_rect.width() : padding_rect.height();
auto scroll_px_per_mouse_position_delta_px = scroll_overflow_size.to_double() / scrollport_size.to_double();
scroll_delta *= scroll_px_per_mouse_position_delta_px;
auto orientation = m_scroll_thumb_dragging_direction == ScrollDirection::Horizontal ? Orientation::Horizontal : Orientation::Vertical;
auto offset_relative_to_gutter = (position - scrollbar_data->gutter_rect.location()).primary_offset_for_orientation(orientation);
auto gutter_size = scrollbar_data->gutter_rect.primary_size_for_orientation(orientation);
auto thumb_size = scrollbar_data->thumb_rect.primary_size_for_orientation(orientation);
// Set the thumb grab position, if we haven't got one already.
if (!m_scroll_thumb_grab_position.has_value()) {
m_scroll_thumb_grab_position = scrollbar_data->thumb_rect.contains(position)
? (position - scrollbar_data->thumb_rect.location()).primary_offset_for_orientation(orientation)
: max(min(offset_relative_to_gutter, thumb_size / 2), offset_relative_to_gutter - gutter_size + thumb_size);
}
// Calculate the relative scroll position (0..1) based on the position of the mouse cursor. We only move the thumb
// if we are interacting with the grab point on the thumb. E.g. if the thumb is all the way to its minimum position
// and the position is beyond the grab point, we should do nothing.
auto constrained_offset = AK::clamp(offset_relative_to_gutter - m_scroll_thumb_grab_position.value(), 0, gutter_size - thumb_size);
auto scroll_position = constrained_offset.to_double() / (gutter_size - thumb_size).to_double();
// Calculate the scroll offset we need to apply to the viewport or element.
auto scrollable_overflow_size = scrollable_overflow_rect()->primary_size_for_orientation(orientation);
auto padding_size = 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));
// Set the new scroll offset.
auto new_scroll_offset = is_viewport() ? document().navigable()->viewport_scroll_offset() : scroll_offset();
new_scroll_offset.set_primary_offset_for_orientation(orientation, scroll_position_in_pixels);
if (is_viewport())
document().window()->scroll_by(scroll_delta.x(), scroll_delta.y());
document().navigable()->perform_scroll_of_viewport(new_scroll_offset);
else
scroll_by(scroll_delta.x(), scroll_delta.y());
m_last_mouse_tracking_position = position;
set_scroll_offset(new_scroll_offset);
}
bool PaintableBox::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned, int wheel_delta_x, int wheel_delta_y)