1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
  235
  236
  237
  238
  239
  240
  241
  242
  243
  244
  245
  246
  247
  248
  249
  250
  251
  252
  253
  254
  255
  256
  257
  258
  259
  260
  261
  262
  263
  264
  265
  266
  267
  268
  269
  270
  271
  272
  273
  274
  275
  276
  277
  278
  279
  280
  281
  282
  283
  284
  285
  286
  287
  288
  289
  290
  291
  292
  293
  294
  295
  296
  297
  298
  299
  300
  301
  302
  303
  304
  305
  306
  307
  308
  309
  310
  311
  312
  313
  314
  315
  316
  317
  318
  319
  320
  321
  322
  323
  324
  325
  326
  327
  328
  329
  330
  331
  332
  333
  334
  335
  336
  337
  338
  339
  340
  341
  342
  343
  344
  345
  346
  347

cc / input / scrollbar_controller.h [blame]

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CC_INPUT_SCROLLBAR_CONTROLLER_H_
#define CC_INPUT_SCROLLBAR_CONTROLLER_H_

#include <memory>
#include <optional>

#include "base/cancelable_callback.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "cc/cc_export.h"
#include "cc/input/input_handler.h"
#include "cc/input/scrollbar.h"
#include "cc/layers/layer_impl.h"
#include "cc/layers/painted_scrollbar_layer_impl.h"

// High level documentation:
// https://source.chromium.org/chromium/chromium/src/+/main:cc/input/README.md

// Click scrolling.
// - A click is considered as a kMouseDown and a kMouseUp in quick succession.
// Every click on a composited non-custom arrow leads to 3 GestureEvents in
// total.
// - GSB and GSU on get queued in the CTEQ on mousedown and a GSE on mouseup.
// - The delta scrolled is constant at 40px (scaled by the device_scale_factor)
// for scrollbar arrows and a function of the viewport length in the case of
// track autoscroll.

// Thumb dragging.
// - The sequence of events in the CTEQ would be something like GSB, GSU, GSU,
// GSU..., GSE
// - On every pointermove, the scroll delta is determined is as current pointer
// position - the point at which we got the initial mousedown.
// - The delta is then scaled by the scroller to scrollbar ratio so that
// dragging the thumb moves the scroller proportionately.
// - This ratio is calculated as:
// (scroll_layer_length - viewport_length) /
// (scrollbar_track_length - scrollbar_thumb_length)
// - On pointerup, the GSE clears state as mentioned above.

// VSync aligned autoscroll.
// - Autoscroll is implemented as a "scroll animation" which has a linear timing
// function (see cc::LinearTimingFunction) and a curve with a constant velocity.
// - The main thread does autoscrolling by pumping events at 50ms interval. To
// have a similar kind of behaviour on the compositor thread, the autoscroll
// velocity is set to 800px per second for scrollbar arrows.
// - For track autoscrolling however, the velocity is a function of the viewport
// length.
// - Based on this velocity, an autoscroll curve is created.
// - An autoscroll animation is set up. (via
// LayerTreeHostImpl::ScrollAnimationCreateInternal) on the the known
// scroll_node and the scroller starts animation when the pointer is held.

// Nuances:
// Thumb snapping.
// - During a thumb drag, if a pointer moves too far away from the scrollbar
// track, the thumb is supposed to snap back to it original place (i.e to the
// point before the thumb drag started).
// - This is done by having an imaginary no_snap_rect around the scrollbar
// track. This extends about 8 times the width of the track on either side. When
// a manipulation is in progress, the mouse is expected to stay within the
// bounds of this rect. Assuming a standard scrollbar, 17px wide, this is how
// it'd look like.
// https://github.com/rahul8805/CompositorThreadedScrollbarDocs/blob/master/snap.PNG?raw=true

// - When a pointerdown is received, record the original offset of the thumb.
// - On every pointermove, check if the pointer is within the bounds of the
// no_snap_rect. If false, snap to the initial_scroll_offset and stop processing
// pointermove(s) until the pointer reenters the bounds of the rect.
// - The moment the mouse re-enters the bounds of the no_snap_rect, we snap to
// the initial_scroll_offset + event.PositionInWidget.

// Thumb anchoring.
// - During a thumb drag, if the pointer runs off the track, there should be no
// additional scrolling until the pointer reenters the track and crosses the
// original mousedown point.
// - This is done by sending "clamped" deltas. The amount of scrollable delta is
// computed using LayerTreeHostImpl::ComputeScrollDelta.
// - Since the deltas are clamped, overscroll doesn't occur if it can't be
// consumed by the CurrentlyScrollingNode.

// Autoscroll play/pause.
// - When the pointer moves in and out of bounds of a scrollbar part that can
// initiate autoscrolls (like arrows or track), the autoscroll animation is
// expected to play or pause accordingly.
// - On every ScrollbarController::WillBeginMainFrame, the pointer location is
// constantly checked and if it is outside the bounds of the scrollbar part that
// initiated the autoscroll, the autoscroll is stopped.
// - Similarly, when the pointer reenters the bounds, autoscroll is restarted
// again. All the vital information during autoscrolling such the velocity,
// direction, scroll layer length etc is held in
// cc::ScrollbarController::AutoscrollState.

// Shift + click.
// - Doing a shift click on any part of a scrollbar track is supposed to do an
// instant scroll to that location (such that the thumb is still centered on the
// pointer).
// - When the MouseEvent reaches the
// InputHandlerProxy::RouteToTypeSpecificHandler, if the event is found to have
// a "Shift" modifier, the ScrollbarController calculates the offset based on
// the pointers current location on the track.
// - Once the offset is determined, the InputHandlerProxy creates a GSU with
// state that tells the LayerTreeHostImpl to perform a non-animated scroll to
// the offset.

// Continuous autoscrolling.
// - This builds on top of the autoscolling implementation. "Continuous"
// autoscrolling is when an autoscroll is in progress and the size of the
// content keeps increasing. For eg: When you keep the down arrow pressed on
// websites like Facebook, the autoscrolling is expected to keep on going until
// the mouse is released.
// - This is implemented by monitoring the length of the scroller layer at every
// frame and if the length increases (and if autoscroll in the forward direction
// is already in progress), the old animation is aborted and a new autoscroll
// animation with the new scroller length is kicked off.

namespace cc {
class LayerTreeHostImpl;

// This class is responsible for hit testing composited scrollbars, event
// handling and creating gesture scroll deltas.
class CC_EXPORT ScrollbarController {
 public:
  explicit ScrollbarController(LayerTreeHostImpl*);
  virtual ~ScrollbarController();

  // On Mac, the "jump to the spot that's clicked" setting can be dynamically
  // set via System Preferences. When enabled, the expectation is that regular
  // clicks on the scrollbar should make the scroller "jump" to the clicked
  // location rather than animated scrolling. Additionally, when this is enabled
  // and the user does an Option + click on the scrollbar, the scroller should
  // *not* jump to that spot (i.e it should be treated as a regular track
  // click). When this setting is disabled on the Mac, Option + click should
  // make the scroller jump and a regular click should animate the scroll
  // offset. On all other platforms, the "jump on click" option is available
  // (via Shift + click) but is not configurable.
  InputHandlerPointerResult HandlePointerDown(
      const gfx::PointF position_in_widget,
      const bool jump_key_modifier);
  InputHandlerPointerResult HandlePointerMove(
      const gfx::PointF position_in_widget);
  InputHandlerPointerResult HandlePointerUp(
      const gfx::PointF position_in_widget);

  bool AutoscrollTaskIsScheduled() const {
    return cancelable_autoscroll_task_ != nullptr;
  }
  bool ScrollbarScrollIsActive() const { return scrollbar_scroll_is_active_; }
  void DidRegisterScrollbar(ElementId element_id,
                            ScrollbarOrientation orientation);
  void DidUnregisterScrollbar(ElementId element_id,
                              ScrollbarOrientation orientation);
  ScrollbarLayerImplBase* ScrollbarLayer() const;
  void WillBeginImplFrame();
  void ResetState();
  const ScrollbarLayerImplBase* HitTest(
      const gfx::PointF position_in_widget) const;

 private:
  FRIEND_TEST_ALL_PREFIXES(LayerTreeHostImplTest, ThumbDragAfterJumpClick);
  FRIEND_TEST_ALL_PREFIXES(LayerTreeHostImplTest,
                           AbortAnimatedScrollBeforeStartingAutoscroll);
  FRIEND_TEST_ALL_PREFIXES(LayerTreeHostImplTest, AutoscrollOnDeletedScrollbar);
  FRIEND_TEST_ALL_PREFIXES(LayerTreeHostImplTest, ScrollOnLargeThumb);

  // "Autoscroll" here means the continuous scrolling that occurs when the
  // pointer is held down on a hit-testable area of the scrollbar such as an
  // arrows of the track itself.
  enum class AutoScrollDirection { kAutoscrollForward, kAutoscrollBackward };

  enum class AutoScrollStatus {
    // For when the 250ms delay before an autoscroll starts animating has not
    // yet elapsed
    kAutoscrollWaiting,
    // For when the delay has elapsed, but the autoscroll cannot animate for
    // some reason (the scrollbar being unregistered)
    kAutoscrollReady,
    // For when the autoscroll is animating
    kAutoscrollScrolling
  };

  struct CC_EXPORT AutoScrollState {
    // Can only be either kAutoscrollForward or kAutoscrollBackward.
    AutoScrollDirection direction = AutoScrollDirection::kAutoscrollForward;

    AutoScrollStatus status = AutoScrollStatus::kAutoscrollWaiting;

    // Stores the autoscroll velocity. The sign is used to set the "direction".
    float velocity = 0.f;

    // Used to track the scroller length while autoscrolling. Helpful for
    // setting up infinite scrolling.
    float scroll_layer_length = 0.f;

    // Used to lookup the rect corresponding to the ScrollbarPart so that
    // autoscroll animations can be played/paused depending on the current
    // pointer location.
    ScrollbarPart pressed_scrollbar_part;
  };

  struct CC_EXPORT DragState {
    // This marks the point at which the drag initiated (relative to the
    // scrollbar layer).
    gfx::PointF drag_origin;

    // This is needed for thumb snapping when the pointer moves too far away
    // from the track while scrolling.
    float scroll_position_at_start_;

    // The |scroller_displacement| indicates the scroll offset compensation that
    // needs to be applied when the scroller's length changes dynamically mid
    // thumb drag. This is needed done to ensure that the scroller does not jump
    // while a thumb drag is in progress.
    float scroller_displacement;
    float scroller_length_at_previous_move;
  };

  struct CC_EXPORT CapturedScrollbarMetadata {
    // Needed to retrieve the ScrollbarSet for a particular ElementId.
    ElementId scroll_element_id;

    // Needed to identify the correct scrollbar from the ScrollbarSet.
    ScrollbarOrientation orientation;
  };

  // Posts an autoscroll task based on the autoscroll state, with the given
  // delay
  void PostAutoscrollTask(const base::TimeDelta delay);

  // Initiates an autoscroll, setting the necessary status and starting the
  // animation, if possible
  void StartAutoScroll();

  // Starts/restarts an autoscroll animation based off of the information in
  // autoscroll_state_
  void StartAutoScrollAnimation();

  // Returns the DSF based on whether use-zoom-for-dsf is enabled.
  float ScreenSpaceScaleFactor() const;

  // Helper to convert scroll offset to autoscroll velocity.
  float InitialDeltaToAutoscrollVelocity(gfx::Vector2dF scroll_delta) const;

  // Returns the hit tested ScrollbarPart based on the position_in_widget.
  ScrollbarPart GetScrollbarPartFromPointerDown(
      const gfx::PointF position_in_widget) const;

  // Clamps |scroll_delta| based on the available scrollable amount of
  // |target_node|. The returned delta includes the page scale factor and is
  // appropriate for use directly as a delta for GSU.
  gfx::Vector2dF ComputeClampedDelta(const ScrollNode& target_node,
                                     const gfx::Vector2dF& scroll_delta) const;

  // Returns the rect for the ScrollbarPart.
  gfx::Rect GetRectForScrollbarPart(const ScrollbarPart scrollbar_part) const;

  LayerImpl* GetLayerHitByPoint(const gfx::PointF position_in_widget) const;

  // Returns scroll delta as Vector2dF based on which ScrollbarPart was hit
  // tested.
  gfx::Vector2dF GetScrollDeltaForScrollbarPart(
      const ScrollbarPart scrollbar_part,
      const bool jump_key_modifier) const;
  // Returns scroll delta in the direction of the scrollbar's orientation.
  float GetScrollDistanceForScrollbarPart(const ScrollbarPart scrollbar_part,
                                          const bool jump_key_modifier) const;

  // Makes position_in_widget relative to the scrollbar.
  gfx::PointF GetScrollbarRelativePosition(const gfx::PointF position_in_widget,
                                           bool* clipped) const;

  // Computes an aritificial drag origin for jump clicks, to give the scrollbar
  // a proper place to snap back to on a jump click then drag
  gfx::PointF DragOriginForJumpClick(
      const ScrollbarLayerImplBase* scrollbar) const;

  // Decides if the scroller should snap to the offset that it was
  // originally at (i.e the offset before the thumb drag).
  bool SnapToDragOrigin(const gfx::PointF pointer_position_in_widget) const;

  // Decides whether a track autoscroll should be aborted (or restarted) due to
  // the thumb reaching the pointer or the pointer leaving (or re-entering) the
  // bounds.
  void RecomputeAutoscrollStateIfNeeded();

  // Shift (or "Option" in case of Mac) + click is expected to do a non-animated
  // jump to a certain offset.
  float GetScrollDistanceForAbsoluteJump() const;

  // Determines if the delta needs to be animated.
  ui::ScrollGranularity Granularity(const ScrollbarPart scrollbar_part,
                                    bool jump_key_modifier) const;

  // Calculates the distance based on position_in_widget and drag_origin.
  float GetScrollDistanceForDragPosition(
      const gfx::PointF pointer_position_in_widget) const;

  // Returns the ratio of the scroller length to the scrollbar length. This is
  // needed to scale the scroll delta for thumb drag.
  float GetScrollerToScrollbarRatio() const;

  float GetViewportLength() const;

  // Returns the page scale factor (i.e. pinch zoom factor). This is relevant
  // for root viewport scrollbar scrolling.
  float GetPageScaleFactorForScroll() const;

  raw_ptr<LayerTreeHostImpl> layer_tree_host_impl_;

  // Used to safeguard against firing GSE without firing GSB and GSU. For
  // example, if mouse is pressed outside the scrollbar but released after
  // moving inside the scrollbar, a GSE will get queued up without this flag.
  bool scrollbar_scroll_is_active_;

  // This is relative to the RenderWidget's origin.
  gfx::PointF last_known_pointer_position_;

  // Set only while interacting with the scrollbar (eg: drag, click etc).
  std::optional<CapturedScrollbarMetadata> captured_scrollbar_metadata_;

  // Holds information pertaining to autoscrolling. This member is empty if and
  // only if an autoscroll is *not* in progress or scheduled
  std::optional<AutoScrollState> autoscroll_state_;

  // Holds information pertaining to thumb drags. Useful while making decisions
  // about thumb anchoring/snapping.
  std::optional<DragState> drag_state_;

  // Used to track if a GSU was processed for the current frame or not. Without
  // this, thumb drag will appear jittery. The reason this happens is because
  // when the first GSU is processed, it gets queued in the compositor thread
  // event queue. So a second request within the same frame will end up
  // calculating an incorrect delta (as ComputeThumbQuadRect would not have
  // accounted for the delta in the first GSU that was not yet dispatched and
  // pointermoves are not VSync aligned).
  bool drag_processed_for_current_frame_;

  std::unique_ptr<base::CancelableOnceClosure> cancelable_autoscroll_task_;
};

}  // namespace cc

#endif  // CC_INPUT_SCROLLBAR_CONTROLLER_H_