File: cull_rect.cc

package info (click to toggle)
chromium 138.0.7204.183-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,908 kB
  • sloc: cpp: 34,937,088; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (550 lines) | stat: -rw-r--r-- 21,855 bytes parent folder | download | duplicates (6)
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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/platform/graphics/paint/cull_rect.h"

#include "base/containers/adapters.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/field_trial_params.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/renderer/platform/graphics/paint/geometry_mapper.h"
#include "third_party/blink/renderer/platform/graphics/paint/scroll_paint_property_node.h"
#include "third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/transforms/affine_transform.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"

namespace blink {

namespace {

constexpr int kReasonablePixelLimit = LayoutUnit::kIntMax;

// This is the size, in css pixels, for which we start using the minimum
// expansion rect if kSmallScrollersUseMinCullRect is enabled.
constexpr int kSmallScrollerArea = 100000;

constexpr int kSmallScrollerChangedEnoughDistance = 512;

bool IsSmallScroller(const TransformPaintPropertyNode& local_transform,
                     float expansion_ratio) {
  if (!features::kSmallScrollersUseMinCullRect.Get()) {
    return false;
  }
  // The small scroller optimization doesn't apply to the root scroller.
  if (local_transform.RequiresCompositingForRootScroller()) {
    return false;
  }
  if (!local_transform.ScrollNode()) {
    return false;
  }

  return local_transform.ScrollNode()->ContainerRect().size().Area64() <=
         kSmallScrollerArea * expansion_ratio * expansion_ratio;
}

int ChangedEnoughMinimumDistance(bool is_small_scroller,
                                 float expansion_ratio) {
  if (is_small_scroller) {
    return kSmallScrollerChangedEnoughDistance * expansion_ratio;
  }
  return features::kCullRectChangedEnoughDistance.Get() * expansion_ratio;
}

int ChangedEnoughMinimumDistance(
    const TransformPaintPropertyNode& local_transform,
    float expansion_ratio) {
  return ChangedEnoughMinimumDistance(
      IsSmallScroller(local_transform, expansion_ratio), expansion_ratio);
}

int MinimumLocalPixelDistanceToExpand(bool is_small_scroller,
                                      float expansion_ratio) {
  // The expansion must be larger than ChangedEnoughMinimumDistance() to
  // prevent unpainted area from being scrolled into the scrollport without
  // repainting. For better user experience, use 2x.
  return 2 * ChangedEnoughMinimumDistance(is_small_scroller, expansion_ratio);
}

// Returns a pair containing:
// - The number of pixels to expand the cull rect for composited scroll
//   and transform. If less than the minimum, returns the mininum.
// - The minimum expansion.
std::pair<int, int> LocalPixelDistanceToExpand(
    const TransformPaintPropertyNode& root_transform,
    const TransformPaintPropertyNode& local_transform,
    float expansion_ratio) {
  const bool is_small_scroller =
      IsSmallScroller(local_transform, expansion_ratio);
  const int min_expansion =
      MinimumLocalPixelDistanceToExpand(is_small_scroller, expansion_ratio);
  if (is_small_scroller) {
    return {min_expansion, min_expansion};
  }

  const int pixel_distance_to_expand =
      features::kCullRectPixelDistanceToExpand.Get();
  int local_pixel_distance_to_expand =
      pixel_distance_to_expand * expansion_ratio;
  if (local_pixel_distance_to_expand == 0) {
    // This can happen when the expansion_ratio is very small.
    return {min_expansion, min_expansion};
  }
  float scale = GeometryMapper::SourceToDestinationApproximateMinimumScale(
      root_transform, local_transform);
  // A very big scale may be caused by non-invertable near non-invertable
  // transforms. Fallback to scale 1. The limit is heuristic.
  if (scale > kReasonablePixelLimit / local_pixel_distance_to_expand) {
    return {local_pixel_distance_to_expand, min_expansion};
  }
  return {std::max<int>(scale * local_pixel_distance_to_expand, min_expansion),
          min_expansion};
}

}  // anonymous namespace

bool CullRect::CanExpandForScroll(const ScrollPaintPropertyNode& scroll) {
  // kNotPreferred is used for selects/inputs which don't benefit from
  // composited scrolling.
  if (scroll.GetCompositedScrollingPreference() ==
      CompositedScrollingPreference::kNotPreferred) {
    return false;
  }
  if (!scroll.UserScrollable()) {
    return false;
  }
  if (scroll.ContentsRect().width() <= scroll.ContainerRect().width() &&
      scroll.ContentsRect().height() <= scroll.ContainerRect().height()) {
    return false;
  }
  return true;
}

bool CullRect::Intersects(const gfx::Rect& rect) const {
  if (rect.IsEmpty())
    return false;
  return IsInfinite() || rect.Intersects(rect_);
}

bool CullRect::IntersectsTransformed(const AffineTransform& transform,
                                     const gfx::RectF& rect) const {
  if (rect.IsEmpty())
    return false;
  return IsInfinite() || transform.MapRect(rect).Intersects(gfx::RectF(rect_));
}

bool CullRect::IntersectsHorizontalRange(LayoutUnit lo, LayoutUnit hi) const {
  return !(lo >= rect_.right() || hi <= rect_.x());
}

bool CullRect::IntersectsVerticalRange(LayoutUnit lo, LayoutUnit hi) const {
  return !(lo >= rect_.bottom() || hi <= rect_.y());
}

void CullRect::Move(const gfx::Vector2d& offset) {
  if (!IsInfinite())
    rect_.Offset(offset);
}

void CullRect::ApplyTransform(const TransformPaintPropertyNode& transform) {
  if (IsInfinite())
    return;

  DCHECK(transform.Parent());
  GeometryMapper::SourceToDestinationRect(*transform.Parent(), transform,
                                          rect_);
}

std::pair<bool, bool> CullRect::ApplyScrollTranslation(
    const TransformPaintPropertyNode& root_transform,
    const TransformPaintPropertyNode& scroll_translation,
    float expansion_ratio) {
  const auto* scroll = scroll_translation.ScrollNode();
  DCHECK(scroll);

  gfx::Rect container_rect = scroll->ContainerRect();
  bool can_expand = expansion_ratio != 0 && CanExpandForScroll(*scroll);
  if (can_expand &&
      RuntimeEnabledFeatures::ScrollCullRectFromContainerRectEnabled()) {
    if (!rect_.Intersects(container_rect)) {
      rect_ = gfx::Rect();
      DVLOG(3) << "No intersection with container rect";
      return {false, false};
    }
    // Always start from the container rect, i.e. ignore ancestor clips.
    // This makes sure the scrolling contents cull rect always cover the
    // container rect, which will ensure the edge-touching logic of
    // ChangedEnough works, and simplify paint checkerboarding reporting for
    // raster-inducing scrolls in cc.
    rect_ = container_rect;
    DVLOG(3) << "Use container rect: " << rect_.ToString();
  } else {
    rect_.Intersect(container_rect);
    if (rect_.IsEmpty()) {
      DVLOG(3) << "No intersection with container rect";
      return {false, false};
    }
    DVLOG(3) << "Clipped by container rect: " << rect_.ToString();
  }
  DVLOG(3) << "Clipped by container rect: " << rect_.ToString();

  ApplyTransform(scroll_translation);
  DVLOG(3) << "Scrolled: " << rect_.ToString();

  if (!can_expand) {
    return {false, false};
  }

  gfx::Rect contents_rect = scroll->ContentsRect();
  // Expand the cull rect for scrolling contents for composited scrolling.
  int outset_x;
  int min_expansion;
  std::tie(outset_x, min_expansion) = LocalPixelDistanceToExpand(
      root_transform, scroll_translation, expansion_ratio);
  int outset_y = outset_x;
  int scroll_range_x = contents_rect.width() - container_rect.width();
  int scroll_range_y = contents_rect.height() - container_rect.height();
  if (scroll_range_x <= 0) {
    outset_x = 0;
  }
  if (scroll_range_y <= 0) {
    outset_y = 0;
  }
  if (outset_x > 0 && outset_y > 0) {
    // If scroller is scrollable in both axes, expand by half to prevent the
    // area of the cull rect from being too big (thus probably too slow to
    // paint and composite).
    outset_x /= 2;
    outset_y /= 2;
    // Give the extra outset beyond scroll range in one axis to the other.
    if (outset_x > scroll_range_x) {
      outset_y += outset_x - scroll_range_x;
    }
    if (outset_y > scroll_range_y) {
      outset_x += outset_y - scroll_range_y;
    }
  }
  // The operations above may have caused the outsets to exceed the scroll
  // range. Trim them back here. Note that we clamp the outset in a single
  // direction to the entire scroll range. Eg, if we have a `scroll_range_x`
  // of 100, we will clamp offset_x to 100, but this will result in both the
  // left and right outset of 100 which means that we will expand the cull
  // rect by 200 in the x dimension. If `rect_` is touching the edge of the
  // contents rect, this will be required on one side (since you can paint a
  // full 100 units into the scroller), but there can be some extra. Commonly,
  // the extra outset will be removed by the intersection with contents_rect
  // below, but it can happen that the original rect is sized and positioned
  // such that the expanded rect won't be adequately clipped by this
  // intersection. This can happen if we are clipped by an ancestor.
  // Note: The clipped-by-ancestor situation doesn't affect
  // ScrollCullRectFromContainerRect. Clean up the comment, maybe also the code
  // when cleaning up the flag.
  outset_x = std::min(std::max(outset_x, min_expansion), scroll_range_x);
  outset_y = std::min(std::max(outset_y, min_expansion), scroll_range_y);
  rect_.Outset(gfx::Outsets::VH(outset_y, outset_x));
  DVLOG(3) << "Expanded(" << outset_x << "," << outset_y
           << "): " << rect_.ToString();

  rect_.Intersect(contents_rect);
  DVLOG(3) << "Clipped by contents_rect: " << rect_.ToString();
  return {outset_x > 0, outset_y > 0};
}

bool CullRect::ApplyPaintPropertiesWithoutExpansion(
    const PropertyTreeState& source,
    const PropertyTreeState& destination) {
  FloatClipRect clip_rect =
      GeometryMapper::LocalToAncestorClipRect(destination, source);
  if (clip_rect.Rect().IsEmpty()) {
    rect_ = gfx::Rect();
    DVLOG(3) << "Empty clip";
    return false;
  }
  if (!clip_rect.IsInfinite()) {
    rect_.Intersect(gfx::ToEnclosingRect(clip_rect.Rect()));
    if (rect_.IsEmpty()) {
      DVLOG(3) << "Empty after clip";
      return false;
    }
  }
  if (!IsInfinite()) {
    GeometryMapper::SourceToDestinationRect(source.Transform(),
                                            destination.Transform(), rect_);
  }
  // Return true even if the transformed rect is empty (e.g. by rotateX(90deg))
  // because later transforms may make the content visible again.
  DVLOG(3) << "ApplyPaintPropertiesWithoutExpansion: " << rect_.ToString();
  return true;
}

bool CullRect::ApplyPaintProperties(
    const PropertyTreeState& root,
    const PropertyTreeState& source,
    const PropertyTreeState& destination,
    const std::optional<CullRect>& old_cull_rect,
    float expansion_ratio) {
  // The caller should check this before calling this function.
  DCHECK_NE(source, destination);

  // Only a clip can make an infinite cull rect finite.
  if (IsInfinite() && &destination.Clip() == &source.Clip())
    return false;

  bool abnormal_hierarchy = !source.Clip().IsAncestorOf(destination.Clip());
  HeapVector<Member<const TransformPaintPropertyNode>, 4> scroll_translations;
  bool has_transform_requiring_expansion = false;

  if (!abnormal_hierarchy) {
    for (const auto* t = &destination.Transform(); t != &source.Transform();
         t = t->UnaliasedParent()) {
      if (t == &root.Transform()) {
        abnormal_hierarchy = true;
        break;
      }
      // TODO(wangxianzhu): This should be DCHECK, but for now we need to work
      // around crbug.com/1262837 etc. Also see the TODO in
      // FragmentData::LocalBorderBoxProperties().
      if (t->IsRoot()) {
        return false;
      }
      if (t->ScrollNode()) {
        scroll_translations.push_back(t);
      } else if (t->RequiresCullRectExpansion()) {
        has_transform_requiring_expansion = true;
      }
    }
  }

  if (abnormal_hierarchy) {
    // Either the transform or the clip of |source| is not an ancestor of
    // |destination|. Map infinite rect from the root.
    *this = Infinite();
    return root != destination &&
           ApplyPaintProperties(root, root, destination, old_cull_rect,
                                expansion_ratio);
  }

  // These are either the source transform/clip or the last scroll
  // translation's transform/clip.
  const auto* last_transform = &source.Transform();
  const auto* last_clip = &source.Clip();
  std::pair<bool, bool> expanded(false, false);
  bool scroll_cull_rect_affected_by_ancestor_clips = false;

  // For now effects (especially pixel-moving filters) are not considered in
  // this class. The client has to use infinite cull rect in the case.
  // TODO(wangxianzhu): support clip rect expansion for pixel-moving filters.
  const auto& effect_root = EffectPaintPropertyNode::Root();
  for (const auto& scroll_translation : base::Reversed(scroll_translations)) {
    const auto* overflow_clip =
        scroll_translation->ScrollNode()->OverflowClipNode();
    if (!overflow_clip) {
      // This happens on the layout viewport scroll node when the viewport
      // doesn't clip contents (e.g. when printing).
      break;
    }
    if (!ApplyPaintPropertiesWithoutExpansion(
            PropertyTreeState(*last_transform, *last_clip, effect_root),
            PropertyTreeState(*scroll_translation->UnaliasedParent(),
                              *overflow_clip, effect_root))) {
      return false;
    }
    last_clip = overflow_clip;

    // We only keep scroll_cull_rect_affected_by_ancestor_clips and expanded of
    // the last scroll translation. We will skip the ChangedEnough logic if the
    // current cull rect doesn't fully cover the scroll container rect.
    scroll_cull_rect_affected_by_ancestor_clips =
        !RuntimeEnabledFeatures::ScrollCullRectFromContainerRectEnabled() &&
        !rect_.Contains(scroll_translation->ScrollNode()->ContainerRect());
    expanded = ApplyScrollTranslation(root.Transform(), *scroll_translation,
                                      expansion_ratio);
    last_transform = scroll_translation;
  }

  if (!ApplyPaintPropertiesWithoutExpansion(
          PropertyTreeState(*last_transform, *last_clip, effect_root),
          destination))
    return false;

  if (IsInfinite())
    return false;

  // Since the cull rect mapping above can produce extremely large numbers in
  // cases of perspective, try our best to "normalize" the result by ensuring
  // that none of the rect dimensions exceed some large, but reasonable, limit.
  // Note that by clamping X and Y, we are effectively moving the rect right /
  // down. However, this will at most make us paint more content, which is
  // better than erroneously deciding that the rect produced here is far
  // offscreen.
  if (rect_.x() < -kReasonablePixelLimit)
    rect_.set_x(-kReasonablePixelLimit);
  if (rect_.y() < -kReasonablePixelLimit)
    rect_.set_y(-kReasonablePixelLimit);
  if (rect_.right() > kReasonablePixelLimit)
    rect_.set_width(kReasonablePixelLimit - rect_.x());
  if (rect_.bottom() > kReasonablePixelLimit)
    rect_.set_height(kReasonablePixelLimit - rect_.y());

  std::optional<gfx::Rect> expansion_bounds;
  if (!scroll_cull_rect_affected_by_ancestor_clips &&
      (expanded.first || expanded.second)) {
    DCHECK(last_transform->ScrollNode());
    expansion_bounds = last_transform->ScrollNode()->ContentsRect();
    if (last_transform != &destination.Transform() ||
        last_clip != &destination.Clip()) {
      // Map expansion_bounds in the same way as we did for rect_ in the last
      // ApplyPaintPropertiesWithoutExpansion().
      FloatClipRect clip_rect = GeometryMapper::LocalToAncestorClipRect(
          destination,
          PropertyTreeState(*last_transform, *last_clip, effect_root));
      if (!clip_rect.IsInfinite())
        expansion_bounds->Intersect(gfx::ToEnclosingRect(clip_rect.Rect()));
      GeometryMapper::SourceToDestinationRect(
          *last_transform, destination.Transform(), *expansion_bounds);
    }
  }

  if (expansion_ratio > 0 && has_transform_requiring_expansion) {
    // Direct compositing reasons such as will-change transform can cause the
    // content to move arbitrarily, so there is no exact cull rect. Instead of
    // using an infinite rect, we use a heuristic of expanding by
    // |pixel_distance_to_expand|. To avoid extreme expansion in the presence
    // of nested composited transforms, the heuristic is skipped for rects that
    // are already very large.
    int pixel_distance_to_expand =
        LocalPixelDistanceToExpand(root.Transform(), destination.Transform(),
                                   expansion_ratio)
            .first;
    if (rect_.width() < pixel_distance_to_expand) {
      rect_.Outset(gfx::Outsets::VH(0, pixel_distance_to_expand));
      if (expansion_bounds)
        expansion_bounds->Outset(gfx::Outsets::VH(0, pixel_distance_to_expand));
      expanded.first = true;
      DVLOG(3) << "Expanded horizontally: " << rect_.ToString();
    }
    if (rect_.height() < pixel_distance_to_expand) {
      rect_.Outset(gfx::Outsets::VH(pixel_distance_to_expand, 0));
      if (expansion_bounds)
        expansion_bounds->Outset(gfx::Outsets::VH(pixel_distance_to_expand, 0));
      expanded.second = true;
      DVLOG(3) << "Expanded vertically: " << rect_.ToString();
    }
  }

  // The edge-touching logic of ChangedEnough is not reliable if the scroll
  // cull rect is affected by ancestor clips.
  if (!scroll_cull_rect_affected_by_ancestor_clips && old_cull_rect &&
      !ChangedEnough(expanded, *old_cull_rect, expansion_bounds,
                     destination.Transform(), expansion_ratio)) {
    rect_ = old_cull_rect->Rect();
    DVLOG(3) << "!ChangedEnough, use old cull rect: " << rect_.ToString();
  }

  return expanded.first || expanded.second;
}

bool CullRect::ChangedEnough(const std::pair<bool, bool>& expanded,
                             const CullRect& old_cull_rect,
                             const std::optional<gfx::Rect>& expansion_bounds,
                             const TransformPaintPropertyNode& local_transform,
                             float expansion_ratio) const {
  const auto& new_rect = Rect();
  const auto& old_rect = old_cull_rect.Rect();
  if (old_rect.IsEmpty() && new_rect.IsEmpty()) {
    return false;
  }

  // Any change in the non-expanded direction should be respected.
  if (!expanded.first &&
      (rect_.x() != old_rect.x() || rect_.width() != old_rect.width())) {
    return true;
  }
  if (!expanded.second &&
      (rect_.y() != old_rect.y() || rect_.height() != old_rect.height())) {
    return true;
  }

  if (old_rect.Contains(new_rect)) {
    return false;
  }
  if (old_rect.IsEmpty()) {
    return true;
  }

  auto old_rect_with_threshold = old_rect;
  old_rect_with_threshold.Outset(
      ChangedEnoughMinimumDistance(local_transform, expansion_ratio));
  if (!old_rect_with_threshold.Contains(new_rect)) {
    return true;
  }

  // The following edge checking logic applies only when the bounds (which were
  // used to clip the cull rect) are known.
  if (!expansion_bounds)
    return false;

  // The cull rect must have been clipped by *expansion_bounds.
  DCHECK(expansion_bounds->Contains(rect_));

  // Even if the new cull rect doesn't include enough new area to satisfy
  // the condition above, update anyway if it touches the edge of the scrolling
  // contents that is not touched by the existing cull rect.  Because it's
  // impossible to expose more area in the direction, update cannot be deferred
  // until the exposed new area satisfies the condition above.
  // For example,
  //   scroller contents dimensions: 100x1000
  //   old cull rect: 0,100 100x8000
  // A new rect of 0,0 100x8000 will not be ChangedEnoughMinimumDistance()
  // pixels away from the current rect. Without additional logic for this case,
  // we will continue using the old cull rect.
  if (rect_.x() == expansion_bounds->x() &&
      old_rect.x() != expansion_bounds->x()) {
    return true;
  }
  if (rect_.y() == expansion_bounds->y() &&
      old_rect.y() != expansion_bounds->y()) {
    return true;
  }
  if (rect_.right() == expansion_bounds->right() &&
      old_rect.right() != expansion_bounds->right()) {
    return true;
  }
  if (rect_.bottom() == expansion_bounds->bottom() &&
      old_rect.bottom() != expansion_bounds->bottom()) {
    return true;
  }

  return false;
}

bool CullRect::HasScrolledEnough(
    const gfx::Vector2dF& delta,
    const TransformPaintPropertyNode& scroll_translation,
    float expansion_ratio) {
  if (!scroll_translation.ScrollNode() ||
      !CanExpandForScroll(*scroll_translation.ScrollNode())) {
    return !delta.IsZero();
  }
  int changed_enough_minimum_distance =
      ChangedEnoughMinimumDistance(scroll_translation, expansion_ratio);
  if (std::abs(delta.x()) < changed_enough_minimum_distance &&
      std::abs(delta.y()) < changed_enough_minimum_distance) {
    return false;
  }

  // Return false if the scroll won't expose more contents in the scrolled
  // direction.
  gfx::Rect contents_rect = scroll_translation.ScrollNode()->ContentsRect();
  if (Rect().Contains(contents_rect))
    return false;
  return (delta.x() < 0 && Rect().x() != contents_rect.x()) ||
         (delta.x() > 0 && Rect().right() != contents_rect.right()) ||
         (delta.y() < 0 && Rect().y() != contents_rect.y()) ||
         (delta.y() > 0 && Rect().bottom() != contents_rect.bottom());
}

}  // namespace blink