Commit d679a163 authored by Daniel Libby's avatar Daniel Libby Committed by Commit Bot

Add main thread percentage scrolling for keyboard and scrollbar

Keyboard arrow scrolling and scrollbar button scrolling should also
scroll a percentage of the scrollable area when percent based scrolling
is enabled.

This CL adds plumbing from the base::Feature to blink, and uses 1/8th as
the percentage to use for directional scrolls via keyboard arrows or
scrollbar buttons. Adds a test suite that runs scrollbar tests in
percentage mode. Keyboard is currently handled on the main thread so we
just use the runtimeFlag toggling via javascript and eventSender.

Bug: 1008153
Change-Id: I7ea7c5471298187da757cf22748620c4accaf192
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2057501
Commit-Queue: Daniel Libby <dlibby@microsoft.com>
Reviewed-by: default avatarKentaro Hara <haraken@chromium.org>
Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#743455}
parent 6e1f28e0
......@@ -14,6 +14,10 @@
static constexpr int kPixelsPerLineStep = 40;
static constexpr float kMinFractionToStepWhenPaging = 0.875f;
// Each directional scroll for percentage-based units should scroll 1/8th of
// the scrollable area.
static constexpr float kPercentDeltaForDirectionalScroll = 0.125f;
// Autoscrolling (on the main thread) happens by applying a delta every 50ms.
// Hence, pixels per second for a autoscroll cc animation can be calculated as:
// autoscroll velocity = delta / 0.05 sec = delta x 20
......
......@@ -305,6 +305,8 @@ void SetRuntimeFeaturesFromChromiumFeatures() {
features::kBrowserVerifiedUserActivationKeyboard, kEnableOnly},
{wf::EnableBrowserVerifiedUserActivationMouse,
features::kBrowserVerifiedUserActivationMouse, kEnableOnly},
{wf::EnablePercentBasedScrolling, features::kPercentBasedScrolling,
kUseFeatureState},
#if defined(OS_ANDROID)
{wf::EnableWebNfc,
features::kWebNfc, kDisableOnly},
......
......@@ -144,6 +144,7 @@ class WebRuntimeFeatures {
bool);
BLINK_PLATFORM_EXPORT static void EnablePaymentApp(bool);
BLINK_PLATFORM_EXPORT static void EnablePaymentRequest(bool);
BLINK_PLATFORM_EXPORT static void EnablePercentBasedScrolling(bool);
BLINK_PLATFORM_EXPORT static void EnablePerformanceManagerInstrumentation(
bool);
BLINK_PLATFORM_EXPORT static void EnablePeriodicBackgroundSync(bool);
......
......@@ -77,25 +77,37 @@ bool MapKeyCodeForScroll(int key_code,
case VKEY_LEFT:
*scroll_direction =
mojom::blink::ScrollDirection::kScrollLeftIgnoringWritingMode;
*scroll_granularity = ScrollGranularity::kScrollByLine;
*scroll_granularity =
RuntimeEnabledFeatures::PercentBasedScrollingEnabled()
? ScrollGranularity::kScrollByPercentage
: ScrollGranularity::kScrollByLine;
*scroll_use_uma = WebFeature::kScrollByKeyboardArrowKeys;
break;
case VKEY_RIGHT:
*scroll_direction =
mojom::blink::ScrollDirection::kScrollRightIgnoringWritingMode;
*scroll_granularity = ScrollGranularity::kScrollByLine;
*scroll_granularity =
RuntimeEnabledFeatures::PercentBasedScrollingEnabled()
? ScrollGranularity::kScrollByPercentage
: ScrollGranularity::kScrollByLine;
*scroll_use_uma = WebFeature::kScrollByKeyboardArrowKeys;
break;
case VKEY_UP:
*scroll_direction =
mojom::blink::ScrollDirection::kScrollUpIgnoringWritingMode;
*scroll_granularity = ScrollGranularity::kScrollByLine;
*scroll_granularity =
RuntimeEnabledFeatures::PercentBasedScrollingEnabled()
? ScrollGranularity::kScrollByPercentage
: ScrollGranularity::kScrollByLine;
*scroll_use_uma = WebFeature::kScrollByKeyboardArrowKeys;
break;
case VKEY_DOWN:
*scroll_direction =
mojom::blink::ScrollDirection::kScrollDownIgnoringWritingMode;
*scroll_granularity = ScrollGranularity::kScrollByLine;
*scroll_granularity =
RuntimeEnabledFeatures::PercentBasedScrollingEnabled()
? ScrollGranularity::kScrollByPercentage
: ScrollGranularity::kScrollByLine;
*scroll_use_uma = WebFeature::kScrollByKeyboardArrowKeys;
break;
case VKEY_HOME:
......
......@@ -279,15 +279,20 @@ bool ScrollManager::LogicalScroll(mojom::blink::ScrollDirection direction,
ScrollableArea* scrollable_area = ScrollableArea::GetForScrolling(box);
DCHECK(scrollable_area);
ScrollOffset delta = ToScrollDelta(physical_direction, 1);
ScrollOffset delta =
ToScrollDelta(physical_direction,
ScrollableArea::DirectionBasedScrollDelta(granularity));
delta.Scale(scrollable_area->ScrollStep(granularity, kHorizontalScrollbar),
scrollable_area->ScrollStep(granularity, kVerticalScrollbar));
// Pressing the arrow key is considered as a scroll with intended direction
// only. Pressing the PgUp/PgDn key is considered as a scroll with intended
// direction and end position. Pressing the Home/End key is considered as a
// scroll with intended end position only.
// only (this results in kScrollByLine or kScrollByPercentage, depending on
// REF::PercentBasedScrollingEnabled). Pressing the PgUp/PgDn key is
// considered as a scroll with intended direction and end position. Pressing
// the Home/End key is considered as a scroll with intended end position
// only.
switch (granularity) {
case ScrollGranularity::kScrollByLine: {
case ScrollGranularity::kScrollByLine:
case ScrollGranularity::kScrollByPercentage: {
if (scrollable_area->SnapForDirection(delta))
return true;
break;
......@@ -319,7 +324,10 @@ bool ScrollManager::LogicalScroll(mojom::blink::ScrollDirection direction,
},
WrapWeakPersistent(scrollable_area)));
ScrollResult result = scrollable_area->UserScroll(
granularity, ToScrollDelta(physical_direction, 1), std::move(callback));
granularity,
ToScrollDelta(physical_direction,
ScrollableArea::DirectionBasedScrollDelta(granularity)),
std::move(callback));
if (result.DidScroll())
return true;
......
......@@ -70,6 +70,13 @@ int ScrollableArea::MaxOverlapBetweenPages() const {
return GetPageScrollbarTheme().MaxOverlapBetweenPages();
}
// static
float ScrollableArea::DirectionBasedScrollDelta(ScrollGranularity granularity) {
return (granularity == ScrollGranularity::kScrollByPercentage)
? kPercentDeltaForDirectionalScroll
: 1;
}
// static
mojom::blink::ScrollBehavior ScrollableArea::DetermineScrollBehavior(
mojom::blink::ScrollBehavior behavior_from_param,
......
......@@ -84,6 +84,10 @@ class CORE_EXPORT ScrollableArea : public GarbageCollectedMixin {
static float MinFractionToStepWhenPaging();
int MaxOverlapBetweenPages() const;
// Returns the amount of delta, in |granularity| units, for a direction-based
// (i.e. keyboard or scrollbar arrow) scroll.
static float DirectionBasedScrollDelta(ScrollGranularity granularity);
// Convert a non-finite scroll value (Infinity, -Infinity, NaN) to 0 as
// per https://drafts.csswg.org/cssom-view/#normalize-non-finite-values.
static float NormalizeNonFiniteScroll(float value) {
......
......@@ -265,8 +265,11 @@ ScrollGranularity Scrollbar::PressedPartScrollGranularity() {
if (pressed_part_ == kBackButtonStartPart ||
pressed_part_ == kBackButtonEndPart ||
pressed_part_ == kForwardButtonStartPart ||
pressed_part_ == kForwardButtonEndPart)
return ScrollGranularity::kScrollByLine;
pressed_part_ == kForwardButtonEndPart) {
return RuntimeEnabledFeatures::PercentBasedScrollingEnabled()
? ScrollGranularity::kScrollByPercentage
: ScrollGranularity::kScrollByLine;
}
return ScrollGranularity::kScrollByPage;
}
......@@ -596,8 +599,10 @@ void Scrollbar::MouseDown(const WebMouseEvent& evt) {
void Scrollbar::InjectScrollGestureForPressedPart(
WebInputEvent::Type gesture_type) {
ScrollOffset delta = ToScrollDelta(PressedPartScrollDirectionPhysical(), 1);
ScrollGranularity granularity = PressedPartScrollGranularity();
ScrollOffset delta =
ToScrollDelta(PressedPartScrollDirectionPhysical(),
ScrollableArea::DirectionBasedScrollDelta(granularity));
InjectScrollGesture(gesture_type, delta, granularity);
}
......
......@@ -344,6 +344,10 @@ void WebRuntimeFeatures::EnablePaymentRequest(bool enable) {
}
}
void WebRuntimeFeatures::EnablePercentBasedScrolling(bool enable) {
RuntimeEnabledFeatures::SetPercentBasedScrollingEnabled(enable);
}
void WebRuntimeFeatures::EnablePerformanceManagerInstrumentation(bool enable) {
RuntimeEnabledFeatures::SetPerformanceManagerInstrumentationEnabled(enable);
}
......
......@@ -1316,6 +1316,10 @@
name: "PaymentRetry",
status: "stable",
},
{
name: "PercentBasedScrolling",
settable_from_internals: true,
},
{
name: "PerformanceManagerInstrumentation",
},
......
......@@ -97,6 +97,8 @@ crbug.com/1012590 [ Linux ] fast/overflow/rtl-scrollbar-drag-origin.html [ Skip
[ Mac ] virtual/scroll_customization/fast/scrolling/scrollbars/scrollbar-thumb-snapping.html [ Skip ]
[ Linux ] virtual/threaded-prefer-compositing/fast/scrolling/scrollbars/scrollbar-thumb-snapping.html [ Skip ]
[ Mac ] virtual/threaded-prefer-compositing/fast/scrolling/scrollbars/scrollbar-thumb-snapping.html [ Skip ]
[ Linux ] virtual/percent-based-scrolling/fast/scrolling/scrollbars/scrollbar-thumb-snapping.html [ Skip ]
[ Mac ] virtual/percent-based-scrolling/fast/scrolling/scrollbars/scrollbar-thumb-snapping.html [ Skip ]
# These tests are specific to Linux.
......
......@@ -298,6 +298,13 @@
"--enable-prefer-compositing-to-lcd-text",
"--disable-smooth-scrolling"]
},
{
"prefix": "percent-based-scrolling",
"bases": ["fast/scrolling/scrollbars"],
"args": ["--enable-features=PercentBasedScrolling",
"--enable-threaded-compositing",
"--enable-prefer-compositing-to-lcd-text"]
},
{
"prefix": "smooth_compositor_threaded_scrollbar_scrolling",
"bases": ["fast/scrolling/scrollbars/scroll-chaining-for-gesture-based-scrolling.html"],
......
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../resources/run-after-layout-and-paint.js"></script>
<div id="scroller" tabindex=1 style="width: 5px; height: 5px; overflow: scroll;">
<div style="width: 1000px; height:1000px;"></div>
</div>
<script>
"use strict";
function runTest() {
if (window.eventSender && window.internals) {
internals.settings.setScrollAnimatorEnabled(false);
internals.settings.setHideScrollbars(true);
internals.runtimeFlags.msExperimentalScrollingEnabled = true;
test(() => {
const scroller = document.getElementById("scroller");
scroller.focus();
assert_equals(document.activeElement.id, "scroller");
eventSender.keyDown("ArrowDown");
eventSender.keyDown("ArrowRight");
assert_greater_than(scroller.scrollTop, 0);
assert_greater_than(scroller.scrollLeft, 0);
}, "Ensure percent-based keyboard scroll can always scroll, even on small scrollers.");
}
}
addEventListener('load', runTest);
</script>
<!DOCTYPE html>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../resources/run-after-layout-and-paint.js"></script>
<div id="outerScroller" style="width: 400px; height: 400px; overflow: scroll;">
<!-- Set tabindex so the scroller is focusable even if layout hasn't yet run -->
<div id="innerScroller" tabindex=1 style="width: 200px; height: 200px; overflow: scroll;">
<div style="width: 1000px; height:1000px;"></div>
</div>
<div style="width: 1000px; height:1000px;"></div>
</div>
<script>
"use strict";
function runTest() {
if (window.eventSender && window.internals) {
internals.settings.setScrollAnimatorEnabled(false);
internals.settings.setHideScrollbars(true);
internals.runtimeFlags.percentBasedScrollingEnabled = true;
test(() => {
const scrollStepsPerViewport = 8;
const innerScroller = document.getElementById("innerScroller");
const outerScroller = document.getElementById("outerScroller");
innerScroller.focus();
assert_equals(document.activeElement.id, "innerScroller");
eventSender.keyDown("ArrowDown");
eventSender.keyDown("ArrowRight");
outerScroller.focus();
assert_equals(document.activeElement.id, "outerScroller");
eventSender.keyDown("ArrowDown");
eventSender.keyDown("ArrowRight");
assert_equals(innerScroller.scrollTop, innerScroller.clientHeight / scrollStepsPerViewport);
assert_equals(innerScroller.scrollLeft, innerScroller.clientWidth / scrollStepsPerViewport);
assert_equals(outerScroller.scrollTop, outerScroller.clientHeight / scrollStepsPerViewport);
assert_equals(outerScroller.scrollLeft, outerScroller.clientWidth / scrollStepsPerViewport);
}, "Scrolls a percentage of the viewport when using the keyboard arrow keys to scroll.");
}
}
addEventListener('load', runTest);
</script>
......@@ -91,7 +91,11 @@ window.onload = () => {
// Click on the Down arrow for standardRectFast.
await mouseClickOn(SCROLLBAR_BUTTON_FWD.x, SCROLLBAR_BUTTON_FWD.y);
await waitForAnimationEndTimeBased(() => {return standardDivFast.scrollTop;});
assert_equals(standardDivFast.scrollTop, 40, "Pressing the down arrow didn't scroll.");
const EXPECTED_SCROLL = (internals.runtimeFlags.percentBasedScrollingEnabled) ?
Math.floor(standardDivFast.clientHeight * SCROLLBAR_SCROLL_PERCENTAGE) : SCROLLBAR_SCROLL_PIXELS;
assert_equals(standardDivFast.scrollTop, EXPECTED_SCROLL, "Pressing the down arrow didn't scroll.");
}, "Test mouse click on non-custom composited div scrollbar arrows.");
promise_test (async () => {
......
......@@ -42,7 +42,9 @@
const TRACK_WIDTH = calculateScrollbarThickness();
const BUTTON_WIDTH = TRACK_WIDTH;
const SCROLL_CORNER = TRACK_WIDTH;
const SCROLL_DELTA = 400;
const SCROLL_DELTA = (internals.runtimeFlags.percentBasedScrollingEnabled) ?
Math.floor(scroller.clientHeight * SCROLLBAR_SCROLL_PERCENTAGE) * 10 :
SCROLLBAR_SCROLL_PIXELS * 10;
const MAX_SCROLLER_OFFSET = 1915;
promise_test (async () => {
......
......@@ -38,6 +38,11 @@ window.onload = () => {
const TRACK_WIDTH = calculateScrollbarThickness();
const BUTTON_WIDTH = TRACK_WIDTH;
const SCROLL_CORNER = TRACK_WIDTH;
assert_equals(standardDivFast.clientHeight, standardDivFast.clientWidth,
"This test assumes that the height and width of 'standardDivFast' are equivalent. If this changes please update SCROLL_AMOUNT to be X/Y specific");
const SCROLL_AMOUNT = internals.runtimeFlags.percentBasedScrollingEnabled ?
Math.floor(standardDivFast.clientHeight * SCROLLBAR_SCROLL_PERCENTAGE) :
SCROLLBAR_SCROLL_PIXELS;
promise_test (async () => {
// Scrollbars on Mac don't have arrows. This test is irrelevant.
......@@ -52,7 +57,7 @@ window.onload = () => {
let y = standardRectFast.bottom - SCROLL_CORNER - BUTTON_WIDTH / 2;
await mouseClickOn(x, y);
await waitForAnimationEndTimeBased(() => {return standardDivFast.scrollTop;});
assert_equals(standardDivFast.scrollTop, 40, "Pressing the down arrow didn't scroll.");
assert_equals(standardDivFast.scrollTop, SCROLL_AMOUNT, "Pressing the down arrow didn't scroll.");
// Click on the Up arrow for standardRectFast.
x = standardRectFast.right - BUTTON_WIDTH / 2;
......@@ -66,7 +71,7 @@ window.onload = () => {
y = standardRectFast.bottom - BUTTON_WIDTH / 2;
await mouseClickOn(x, y);
await waitForAnimationEndTimeBased(() => {return standardDivFast.scrollLeft;});
assert_equals(standardDivFast.scrollLeft, 40, "Pressing the right arrow didn't scroll.");
assert_equals(standardDivFast.scrollLeft, SCROLL_AMOUNT, "Pressing the right arrow didn't scroll.");
// Click on the Left arrow for standardRectFast.
x = standardRectFast.left + BUTTON_WIDTH / 2;
......
......@@ -13,6 +13,10 @@ window.onload = () => {
const TRACK_WIDTH = calculateScrollbarThickness();
const BUTTON_WIDTH = TRACK_WIDTH;
const SCROLL_CORNER = TRACK_WIDTH;
const SCROLL_AMOUNT_Y = internals.runtimeFlags.percentBasedScrollingEnabled ?
Math.floor(document.scrollingElement.clientHeight * SCROLLBAR_SCROLL_PERCENTAGE) : SCROLLBAR_SCROLL_PIXELS;
const SCROLL_AMOUNT_X = internals.runtimeFlags.percentBasedScrollingEnabled ?
Math.floor(document.scrollingElement.clientWidth * SCROLLBAR_SCROLL_PERCENTAGE) : SCROLLBAR_SCROLL_PIXELS;
promise_test (async (test) => {
// Scrollbars on Mac don't have arrows. This test is irrelevant.
......@@ -27,7 +31,7 @@ window.onload = () => {
let y = window.innerHeight - SCROLL_CORNER - BUTTON_WIDTH / 2;
await mouseClickOn(x, y);
await waitForAnimationEndTimeBased(() => {return window.scrollY;});
assert_equals(window.scrollY, 40, "Pressing the down arrow didn't scroll.");
assert_equals(window.scrollY, SCROLL_AMOUNT_Y, "Pressing the down arrow didn't scroll.");
// Click on the Up arrow of the viewport.
x = window.innerWidth - BUTTON_WIDTH / 2;
......@@ -41,7 +45,7 @@ window.onload = () => {
y = window.innerHeight - BUTTON_WIDTH / 2;
await mouseClickOn(x, y);
await waitForAnimationEndTimeBased(() => {return window.scrollX;});
assert_equals(window.scrollX, 40, "Pressing the right arrow didn't scroll.");
assert_equals(window.scrollX, SCROLL_AMOUNT_X, "Pressing the right arrow didn't scroll.");
// Click on the Left arrow of the viewport.
x = BUTTON_WIDTH / 2;
......
......@@ -81,4 +81,4 @@
}, 'Drag a scrollbar and release it on another DIV, only the DIV owns the' +
' dragging scrollbar receive mouse events');
}
</script>
\ No newline at end of file
</script>
......@@ -66,7 +66,9 @@
// Remove the div and verify that scrolling takes place as expected.
document.body.removeChild(occlusion_div);
await mouseClickOn(down_arrow_x, down_arrow_y);
const expected = onMacPlatform ? 74 : 40;
const EXPECTED_BUTTON_SCROLL_AMOUNT = internals.runtimeFlags.percentBasedScrollingEnabled ?
Math.floor(scroller.clientHeight * SCROLLBAR_SCROLL_PERCENTAGE) : SCROLLBAR_SCROLL_PIXELS;
const expected = onMacPlatform ? 74 : EXPECTED_BUTTON_SCROLL_AMOUNT;
const pressedPart = onMacPlatform ? "track" : "down arrow";
await waitForAnimationEndTimeBased(() => {return scroller.scrollTop;});
assert_equals(scroller.scrollTop, expected, "Pressing the " + pressedPart + " didn't scroll.");
......
......@@ -37,4 +37,12 @@ function resetScrollOffset(scrollElement) {
}
}
// TODO(arakeri): Add helpers for arrow widths.
\ No newline at end of file
// The percentage scrollbar arrows will scroll, if percent-based scrolling
// is enabled.
const SCROLLBAR_SCROLL_PERCENTAGE = 0.125;
// The number of pixels scrollbar arrows will scroll when percent-based
// scrolling is not enabled.
const SCROLLBAR_SCROLL_PIXELS = 40;
// TODO(arakeri): Add helpers for arrow widths.
This directory is dedicated for testing the "Percent-based scrolling" feature.
This directory is dedicated for testing the "Percent-based scrolling" feature.
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