Commit de51bb9c authored by Sam Fortiner's avatar Sam Fortiner Committed by Commit Bot

Expand interaction gutters for scrollbars

There is a gutter rect around the scrollbar during which thumb drags
will behave as though the mouse is on the thumb even if it moves off the
thumb but is still inside the gutter rect.  If the mouse moves outside
of that gutter rect, the thumb snaps back to its original position.
This behavior offers users the ability to quickly toggle between two
positions in a scroller by intentionally moving the thumb in the
non-scrolling direction in and out of the gutter.  However, it also can
frustrate users if they grabbed the scrollbar thumb and dragged in the
scrolling direction but unintentionally move outside of the gutter
causing the thumb to snap back.

This change modifies the behavior so that scrollbars have an infinitely
large interaction gutter rect in the scrolling direction.

Bug: 801188
Change-Id: I838741053305122adf5b81345eb56d87916edd0e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1566027
Commit-Queue: Sam Fortiner <samfort@microsoft.com>
Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#652399}
parent 77895c86
......@@ -351,35 +351,40 @@ bool ScrollbarThemeAura::ShouldSnapBackToDragOrigin(
return false;
#endif
// Constants used to figure the drag rect outside which we should snap the
// scrollbar thumb back to its origin. These calculations are based on
// observing the behavior of the MSVC8 main window scrollbar + some
// guessing/extrapolation.
static const int kOffEndMultiplier = 3;
// There is a drag rect around the scrollbar outside of which the scrollbar
// thumb should snap back to its origin. This rect is infinitely large in
// the scrollbar's scrolling direction and an expansion of the scrollbar's
// width or height in the non-scrolling direction.
// Constants used to figure the how far out in the non-scrolling direction
// should trigger the thumb to snap back to its origin. These calculations
// are based on observing the behavior of the MSVC8 main window scrollbar +
// some guessing/extrapolation.
static const int kOffSideMultiplier = 8;
static const int kDefaultWinScrollbarThickness = 17;
// Find the rect within which we shouldn't snap, by expanding the track rect
// in both dimensions.
IntRect no_snap_rect(TrackRect(scrollbar));
// As only one axis triggers snapping back, the code below only uses the
// thickness of the scrollbar for its calculations.
bool is_horizontal = scrollbar.Orientation() == kHorizontalScrollbar;
int thickness = is_horizontal ? no_snap_rect.Height() : no_snap_rect.Width();
int thickness = is_horizontal ? TrackRect(scrollbar).Height()
: TrackRect(scrollbar).Width();
// Even if the platform's scrollbar is narrower than the default Windows one,
// we still want to provide at least as much slop area, since a slightly
// narrower scrollbar doesn't necessarily imply that users will drag
// straighter.
thickness = std::max(thickness, kDefaultWinScrollbarThickness);
int width_outset =
(is_horizontal ? kOffEndMultiplier : kOffSideMultiplier) * thickness;
int height_outset =
(is_horizontal ? kOffSideMultiplier : kOffEndMultiplier) * thickness;
no_snap_rect.Expand(
IntRectOutsets(height_outset, width_outset, height_outset, width_outset));
int expansion_amount =
kOffSideMultiplier * std::max(thickness, kDefaultWinScrollbarThickness);
int snap_outside_of_min = -expansion_amount;
int snap_outside_of_max = expansion_amount + thickness;
IntPoint mouse_position = scrollbar.ConvertFromRootFrame(
FlooredIntPoint(event.PositionInRootFrame()));
mouse_position.Move(scrollbar.X(), scrollbar.Y());
return !no_snap_rect.Contains(mouse_position);
int mouse_offset_in_scrollbar =
is_horizontal ? mouse_position.Y() : mouse_position.X();
return (mouse_offset_in_scrollbar < snap_outside_of_min ||
mouse_offset_in_scrollbar >= snap_outside_of_max);
}
bool ScrollbarThemeAura::HasScrollbarButtons(
......
<!DOCTYPE html>
<title>Scrollbar interaction gutter</title>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<style>
#container {
position: absolute;
left: 200px;
top: 195px;
width: 300px;
height: 200px;
overflow: scroll;
}
#content {
width: 1000px;
height: 1000px;
}
</style>
<div id="container">
<div id="content">
</div>
</div>
This test verifies scroll position restores/snaps correctly when a thumb drag
has been cancelled.<br/>
The general behavior is that the cancelling behavior is only for the
scrollbar's non-scrolling direction.<br/>
This test is expected to behave differently on Mac and Linux because those
platforms don't cancel scrolling when mouse cursor is out of a certain
range.<br/>
<script>
const leftButton = 0;
function dragWithoutUp(from, to) {
assert_own_property(window, 'chrome');
assert_own_property(window.chrome, 'gpuBenchmarking');
return new Promise((resolve) => {
chrome.gpuBenchmarking.pointerActionSequence(
[{
source: 'mouse',
actions: [
{name: 'pointerMove', x: from.x, y: from.y},
{name: 'pointerDown', x: from.x, y: from.y, button: leftButton},
{name: 'pointerMove', x: to.x, y: to.y}
]
}],
resolve);
});
}
function sendUp(at) {
assert_own_property(window, 'chrome');
assert_own_property(window.chrome, 'gpuBenchmarking');
return new Promise((resolve) => {
chrome.gpuBenchmarking.pointerActionSequence(
[{
source: 'mouse',
actions: [
// Need to send a down with the up or pointerActionSequence will
// refuse to send the up because the sequence believes the button
// is not currently down.
{name: 'pointerDown', x: at.x, y: at.y, button: leftButton},
{name: 'pointerUp', button: leftButton}
]
}],
resolve);
});
}
function resetScroller() {
var container = document.getElementById("container");
container.scrollLeft = 350;
container.scrollTop = 350;
}
var onMacPlatform = navigator.userAgent.search(/\bMac OS X\b/) != -1;
var onLinuxPlatform = navigator.userAgent.search(/\bLinux\b/) != -1;
promise_test(async t => {
let from = { x: 490, y: 285 };
let to = { x: 490, y: 50 };
t.add_cleanup(async c => { await sendUp(to); });
resetScroller();
await dragWithoutUp(from, to);
assert_equals(container.scrollTop, 0);
}, document.title + ', drag vertical scrollbar out top');
promise_test(async t => {
let from = { x: 490, y: 285 };
let to = { x: 490, y: 560 };
t.add_cleanup(async c => { await sendUp(to); });
resetScroller();
await dragWithoutUp(from, to);
assert_equals(container.scrollTop, 815);
}, document.title + ', drag vertical scrollbar out bottom');
promise_test(async t => {
let from = { x: 490, y: 285 };
let to = { x: 230, y: 560 };
t.add_cleanup(async c => { await sendUp(to); });
resetScroller();
await dragWithoutUp(from, to);
if (onMacPlatform || onLinuxPlatform) {
// Mac and Linux/Android should not snap
assert_equals(container.scrollTop, 815);
}
else {
// Dragging out left should cause snapping even though we're not
// snapping due to the vertical position.
assert_equals(container.scrollTop, 350);
}
}, document.title + ', drag vertical scrollbar out left');
promise_test(async t => {
let from = { x: 490, y: 285 };
let to = { x: 650, y: 560 };
t.add_cleanup(async c => { await sendUp(to); });
resetScroller();
await dragWithoutUp(from, to);
if (onMacPlatform || onLinuxPlatform) {
// Mac and Linux/Android should not snap
assert_equals(container.scrollTop, 815);
}
else {
// Dragging out right should cause snapping even though we're not
// snapping due to the vertical position.
assert_equals(container.scrollTop, 350);
}
}, document.title + ', drag vertical scrollbar out right');
promise_test(async t => {
let from = { x: 340, y: 390 };
let to = { x: 25, y: 390 };
t.add_cleanup(async c => { await sendUp(to); });
resetScroller();
await dragWithoutUp(from, to);
assert_equals(container.scrollLeft, 0);
}, document.title + ', drag horizontal scrollbar out left');
promise_test(async t => {
let from = { x: 340, y: 390 };
let to = { x: 560, y: 390 };
t.add_cleanup(async c => { await sendUp(to); });
resetScroller();
await dragWithoutUp(from, to);
assert_equals(container.scrollLeft, 715);
}, document.title + ', drag horizontal scrollbar out right');
promise_test(async t => {
let from = { x: 340, y: 390 };
let to = { x: 560, y: 190 };
t.add_cleanup(async c => { await sendUp(to); });
resetScroller();
await dragWithoutUp(from, to);
if (onMacPlatform || onLinuxPlatform) {
// Mac and Linux/Android should not snap
assert_equals(container.scrollLeft, 715);
}
else {
// Dragging out the top should cause snapping even though we're not
// snapping due to the right position.
assert_equals(container.scrollLeft, 350);
}
}, document.title + ', drag horizontal scrollbar out top');
promise_test(async t => {
let from = { x: 340, y: 390 };
let to = { x: 560, y: 590 };
t.add_cleanup(async c => { await sendUp(to); });
resetScroller();
await dragWithoutUp(from, to);
if (onMacPlatform || onLinuxPlatform) {
// Mac and Linux/Android should not snap
assert_equals(container.scrollLeft, 715);
}
else {
// Dragging out the bottom should cause snapping even though we're not
// snapping due to the right position.
assert_equals(container.scrollLeft, 350);
}
}, document.title + ', drag horizontal scrollbar out bottom');
</script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment