From e56146edecc247d3fbae3e7de3c131b6be66e69b Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Wed, 18 Jun 2025 12:00:21 +0200 Subject: [PATCH] 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. --- Libraries/LibWeb/Painting/PaintableBox.cpp | 71 ++++++++++++---------- Libraries/LibWeb/Painting/PaintableBox.h | 4 +- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index b072793c7b8..f618ad3d870 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -916,24 +916,15 @@ Paintable::DispatchEventOfSameName PaintableBox::handle_mousedown(Badgethumb_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, 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(*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(Badgegutter_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 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, CSSPixelPoint, unsigned, unsigned, int wheel_delta_x, int wheel_delta_y) diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index 04770c9ddbe..c6a3e2e3f80 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -279,7 +279,7 @@ private: virtual DispatchEventOfSameName handle_mousemove(Badge, CSSPixelPoint, unsigned buttons, unsigned modifiers) override; bool scrollbar_contains_mouse_position(ScrollDirection, CSSPixelPoint); - void scroll_to_mouse_postion(CSSPixelPoint); + void scroll_to_mouse_position(CSSPixelPoint); OwnPtr m_stacking_context; @@ -305,7 +305,7 @@ private: Optional m_outline_data; CSSPixels m_outline_offset { 0 }; - Optional m_last_mouse_tracking_position; + Optional m_scroll_thumb_grab_position; Optional m_scroll_thumb_dragging_direction; bool m_draw_enlarged_horizontal_scrollbar { false }; bool m_draw_enlarged_vertical_scrollbar { false };