ScrollThumbUtils.cpp (15533B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "ScrollThumbUtils.h" 8 #include "AsyncPanZoomController.h" 9 #include "FrameMetrics.h" 10 #include "UnitTransforms.h" 11 #include "Units.h" 12 #include "gfxPlatform.h" 13 #include "mozilla/gfx/Matrix.h" 14 #include "mozilla/StaticPrefs_toolkit.h" 15 16 namespace mozilla { 17 namespace layers { 18 namespace apz { 19 20 struct AsyncScrollThumbTransformer { 21 // Inputs 22 const LayerToParentLayerMatrix4x4& mCurrentTransform; 23 const gfx::Matrix4x4& mScrollableContentTransform; 24 AsyncPanZoomController* mApzc; 25 const FrameMetrics& mMetrics; 26 const ScrollbarData& mScrollbarData; 27 bool mScrollbarIsDescendant; 28 29 // Intermediate results 30 AsyncTransformComponentMatrix mAsyncTransform; 31 AsyncTransformComponentMatrix mScrollbarTransform; 32 33 LayerToParentLayerMatrix4x4 ComputeTransform(); 34 35 private: 36 // Helper functions for ComputeTransform(). 37 38 // If the thumb's orientation is along |aAxis|, add transformations 39 // of the thumb into |mScrollbarTransform|. 40 void ApplyTransformForAxis(const Axis& aAxis); 41 42 enum class ScrollThumbExtent { Start, End }; 43 44 // Scale the thumb by |aScale| along |aAxis|, while keeping constant the 45 // position of the top denoted by |aExtent|. 46 void ScaleThumbBy(const Axis& aAxis, float aScale, ScrollThumbExtent aExtent); 47 48 // Translate the thumb along |aAxis| by |aTranslation| in "scrollbar space" 49 // (CSS pixels along the scrollbar track, similar to e.g. 50 // |mScrollbarData.mThumbStart|). 51 void TranslateThumb(const Axis& aAxis, OuterCSSCoord aTranslation); 52 }; 53 54 void AsyncScrollThumbTransformer::TranslateThumb(const Axis& aAxis, 55 OuterCSSCoord aTranslation) { 56 aAxis.PostTranslate( 57 mScrollbarTransform, 58 ViewAs<CSSPixel>(aTranslation, 59 PixelCastJustification::CSSPixelsOfSurroundingContent) * 60 mMetrics.GetDevPixelsPerCSSPixel() * 61 LayoutDeviceToParentLayerScale(1.0)); 62 } 63 64 void AsyncScrollThumbTransformer::ScaleThumbBy(const Axis& aAxis, float aScale, 65 ScrollThumbExtent aExtent) { 66 // To keep the position of the top of the thumb constant, the thumb needs to 67 // translated to compensate for the scale applied. The origin with respect to 68 // which the scale is applied is the origin of the layer tree, rather than 69 // the origin of the scroll thumb. This means that the space between the 70 // origin and the top of thumb (including the part of the scrollbar track 71 // above the thumb, the part of the scrollbar above the track (i.e. a 72 // scrollbar button if present), plus whatever content is above the scroll 73 // frame) is scaled too, effectively translating the thumb. We undo that 74 // translation here. (One can think of the adjustment being done to the 75 // translation here as a change of basis. We have a method to help with that, 76 // Matrix4x4::ChangeBasis(), but it wouldn't necessarily make the code cleaner 77 // in this case). 78 const OuterCSSCoord scrollTrackOrigin = 79 aAxis.GetPointOffset( 80 mMetrics.CalculateCompositionBoundsInOuterCssPixels().TopLeft()) + 81 mScrollbarData.mScrollTrackStart; 82 OuterCSSCoord thumbExtent = scrollTrackOrigin + mScrollbarData.mThumbStart; 83 if (aExtent == ScrollThumbExtent::End) { 84 thumbExtent += mScrollbarData.mThumbLength; 85 } 86 const OuterCSSCoord thumbExtentScaled = thumbExtent * aScale; 87 const OuterCSSCoord thumbExtentDelta = thumbExtentScaled - thumbExtent; 88 89 aAxis.PostScale(mScrollbarTransform, aScale); 90 TranslateThumb(aAxis, -thumbExtentDelta); 91 } 92 93 void AsyncScrollThumbTransformer::ApplyTransformForAxis(const Axis& aAxis) { 94 ParentLayerCoord asyncScroll = aAxis.GetTransformTranslation(mAsyncTransform); 95 const float asyncZoom = aAxis.GetTransformScale(mAsyncTransform); 96 const ParentLayerCoord overscroll = 97 aAxis.GetPointOffset(mApzc->GetOverscrollAmount()); 98 99 bool haveAsyncZoom = !FuzzyEqualsAdditive(asyncZoom, 1.f); 100 if (!haveAsyncZoom && mApzc->IsZero(asyncScroll) && 101 mApzc->IsZero(overscroll)) { 102 return; 103 } 104 105 OuterCSSCoord translation; 106 float scale = 1.0; 107 108 bool recalcMode = StaticPrefs::apz_scrollthumb_recalc(); 109 if (recalcMode) { 110 // In this branch (taken when apz.scrollthumb.recalc=true), |translation| 111 // and |scale| are computed using the approach implemented in bug 1554795 112 // of fully recalculating the desired position and size using the logic 113 // that attempts to closely match the main-thread calculation. 114 115 const CSSRect visualViewportRect = mApzc->GetCurrentAsyncVisualViewport( 116 AsyncPanZoomController::eForCompositing); 117 const CSSCoord visualViewportLength = 118 aAxis.GetRectLength(visualViewportRect); 119 120 const CSSCoord maxMinPosDifference = 121 CSSCoord( 122 aAxis.GetRectLength(mMetrics.GetScrollableRect()).Truncated()) - 123 visualViewportLength; 124 125 OuterCSSCoord effectiveThumbLength = mScrollbarData.mThumbLength; 126 127 if (haveAsyncZoom) { 128 // The calculations here closely follow the main thread calculations at 129 // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/generic/nsGfxScrollFrame.cpp#6902-6927 130 // and 131 // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/xul/nsSliderFrame.cpp#587-614 132 // Any modifications there should be reflected here as well. 133 const CSSCoord pageIncrementMin = 134 static_cast<int>(visualViewportLength * 0.8); 135 CSSCoord pageIncrement; 136 137 CSSToLayoutDeviceScale deviceScale = mMetrics.GetDevPixelsPerCSSPixel(); 138 if (*mScrollbarData.mDirection == ScrollDirection::eVertical) { 139 const CSSCoord lineScrollAmount = 140 (mApzc->GetScrollMetadata().GetLineScrollAmount() / deviceScale) 141 .height; 142 const double kScrollMultiplier = 143 StaticPrefs::toolkit_scrollbox_verticalScrollDistance(); 144 CSSCoord increment = lineScrollAmount * kScrollMultiplier; 145 146 pageIncrement = 147 std::max(visualViewportLength - increment, pageIncrementMin); 148 } else { 149 pageIncrement = pageIncrementMin; 150 } 151 152 float ratio = pageIncrement / (maxMinPosDifference + pageIncrement); 153 154 OuterCSSCoord desiredThumbLength{ 155 std::max(mScrollbarData.mThumbMinLength, 156 mScrollbarData.mScrollTrackLength * ratio)}; 157 158 // Round the thumb length to an integer number of LayoutDevice pixels, to 159 // match the main-thread behaviour. 160 auto outerDeviceScale = ViewAs<OuterCSSToLayoutDeviceScale>( 161 deviceScale, PixelCastJustification::CSSPixelsOfSurroundingContent); 162 desiredThumbLength = 163 LayoutDeviceCoord((desiredThumbLength * outerDeviceScale).Rounded()) / 164 outerDeviceScale; 165 166 effectiveThumbLength = desiredThumbLength; 167 168 scale = desiredThumbLength / mScrollbarData.mThumbLength; 169 } 170 171 // Subtracting the offset of the scrollable rect is needed for right-to-left 172 // pages. 173 const CSSCoord curPos = aAxis.GetRectOffset(visualViewportRect) - 174 aAxis.GetRectOffset(mMetrics.GetScrollableRect()); 175 176 const CSSToOuterCSSScale thumbPosRatio( 177 (maxMinPosDifference != 0) 178 ? float((mScrollbarData.mScrollTrackLength - effectiveThumbLength) / 179 maxMinPosDifference) 180 : 1.f); 181 182 const OuterCSSCoord desiredThumbPos = curPos * thumbPosRatio; 183 184 translation = desiredThumbPos - mScrollbarData.mThumbStart; 185 } else { 186 // In this branch (taken when apz.scrollthumb.recalc=false), |translation| 187 // and |scale| are computed using the pre-bug1554795 approach of turning 188 // the async scroll and zoom deltas into transforms to apply to the 189 // main-thread thumb position and size. 190 191 // The scroll thumb needs to be scaled in the direction of scrolling by the 192 // inverse of the async zoom. This is because zooming in decreases the 193 // fraction of the whole srollable rect that is in view. 194 scale = 1.f / asyncZoom; 195 196 // Note: |metrics.GetZoom()| doesn't yet include the async zoom. 197 CSSToParentLayerScale effectiveZoom = 198 CSSToParentLayerScale(mMetrics.GetZoom().scale * asyncZoom); 199 200 if (gfxPlatform::UseDesktopZoomingScrollbars()) { 201 // As computed by GetCurrentAsyncTransform, asyncScrollY is 202 // asyncScrollY = -(GetEffectiveScrollOffset - 203 // mLastContentPaintMetrics.GetLayoutScrollOffset()) * 204 // effectiveZoom 205 // where GetEffectiveScrollOffset includes the visual viewport offset that 206 // the main thread knows about plus any async scrolling to the visual 207 // viewport offset that the main thread does not (yet) know about. We want 208 // asyncScrollY to be 209 // asyncScrollY = -(GetEffectiveScrollOffset - 210 // mLastContentPaintMetrics.GetVisualScrollOffset()) * effectiveZoom 211 // because the main thread positions the scrollbars at the visual viewport 212 // offset that it knows about. (aMetrics is mLastContentPaintMetrics) 213 214 asyncScroll -= aAxis.GetPointOffset((mMetrics.GetLayoutScrollOffset() - 215 mMetrics.GetVisualScrollOffset()) * 216 effectiveZoom); 217 } 218 219 // Here we convert the scrollbar thumb ratio into a true unitless ratio by 220 // dividing out the conversion factor from the scrollframe's parent's space 221 // to the scrollframe's space. 222 float unitlessThumbRatio = mScrollbarData.mThumbRatio / 223 (mMetrics.GetPresShellResolution() * asyncZoom); 224 225 // The scroll thumb needs to be translated in opposite direction of the 226 // async scroll. This is because scrolling down, which translates the layer 227 // content up, should result in moving the scroll thumb down. 228 ParentLayerCoord translationPL = -asyncScroll * unitlessThumbRatio; 229 230 // The translation we computed is in the scroll frame's ParentLayer space. 231 // This includes the full cumulative resolution, even if we are a subframe. 232 // However, the resulting transform is used in a context where the scrollbar 233 // is already subject to the resolutions of enclosing scroll frames. To 234 // avoid double application of these enclosing resolutions, divide them out, 235 // leaving only the local resolution if any. 236 translationPL /= (mMetrics.GetCumulativeResolution().scale / 237 mMetrics.GetPresShellResolution()); 238 239 // Convert translation to CSS pixels as this is what TranslateThumb expects. 240 translation = ViewAs<OuterCSSPixel>( 241 translationPL / (mMetrics.GetDevPixelsPerCSSPixel() * 242 LayoutDeviceToParentLayerScale(1.0)), 243 PixelCastJustification::CSSPixelsOfSurroundingContent); 244 } 245 246 // When scaling the thumb to account for the async zoom, keep the position 247 // of the start of the thumb (which corresponds to the scroll offset) 248 // constant. 249 if (haveAsyncZoom) { 250 ScaleThumbBy(aAxis, scale, ScrollThumbExtent::Start); 251 } 252 253 // If the page is overscrolled, additionally squish the thumb in accordance 254 // with the overscroll amount. 255 if (overscroll != 0) { 256 ParentLayerCoord compBoundsLength = 257 aAxis.GetRectLength(mMetrics.GetCompositionBounds()); 258 259 // It's possible for the overscroll amount to be larger than the length 260 // of the composition bounds, if an overscroll animation was started with 261 // a very large velocity. 262 float overscrollProportion = 263 std::min(std::abs(overscroll.value), compBoundsLength.value) / 264 compBoundsLength.value; 265 266 float overscrollScale = 1.0f - overscrollProportion; 267 MOZ_ASSERT(overscrollScale >= 0.0f && overscrollScale <= 1.0f); 268 // If we're overscrolled at the top, keep the top of the thumb in place 269 // as we squish it. If we're overscrolled at the bottom, keep the bottom of 270 // the thumb in place. 271 ScaleThumbBy( 272 aAxis, overscrollScale, 273 overscroll < 0 ? ScrollThumbExtent::Start : ScrollThumbExtent::End); 274 } 275 276 TranslateThumb(aAxis, translation); 277 } 278 279 LayerToParentLayerMatrix4x4 AsyncScrollThumbTransformer::ComputeTransform() { 280 // We only apply the transform if the scroll-target layer has non-container 281 // children (i.e. when it has some possibly-visible content). This is to 282 // avoid moving scroll-bars in the situation that only a scroll information 283 // layer has been built for a scroll frame, as this would result in a 284 // disparity between scrollbars and visible content. 285 if (mMetrics.IsScrollInfoLayer()) { 286 return LayerToParentLayerMatrix4x4{}; 287 } 288 289 MOZ_RELEASE_ASSERT(mApzc); 290 291 mAsyncTransform = 292 mApzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing); 293 294 // |mAsyncTransform| represents the amount by which we have scrolled and 295 // zoomed since the last paint. Because the scrollbar was sized and positioned 296 // based on the painted content, we need to adjust it based on asyncTransform 297 // so that it reflects what the user is actually seeing now. 298 if (*mScrollbarData.mDirection == ScrollDirection::eVertical) { 299 ApplyTransformForAxis(mApzc->mY); 300 } 301 if (*mScrollbarData.mDirection == ScrollDirection::eHorizontal) { 302 ApplyTransformForAxis(mApzc->mX); 303 } 304 305 LayerToParentLayerMatrix4x4 transform = 306 mCurrentTransform * mScrollbarTransform; 307 308 AsyncTransformComponentMatrix compensation; 309 // If the scrollbar layer is a child of the content it is a scrollbar for, 310 // then we need to adjust for any async transform (including an overscroll 311 // transform) on the content. This needs to be cancelled out because layout 312 // positions and sizes the scrollbar on the assumption that there is no async 313 // transform, and without this adjustment the scrollbar will end up in the 314 // wrong place. 315 // 316 // Note that since the async transform is applied on top of the content's 317 // regular transform, we need to make sure to unapply the async transform in 318 // the same coordinate space. This requires applying the content transform 319 // and then unapplying it after unapplying the async transform. 320 if (mScrollbarIsDescendant) { 321 AsyncTransformComponentMatrix overscroll = 322 mApzc->GetOverscrollTransform(AsyncPanZoomController::eForCompositing); 323 gfx::Matrix4x4 asyncUntransform = 324 (mAsyncTransform * overscroll).Inverse().ToUnknownMatrix(); 325 const gfx::Matrix4x4& contentTransform = mScrollableContentTransform; 326 gfx::Matrix4x4 contentUntransform = contentTransform.Inverse(); 327 328 compensation *= ViewAs<AsyncTransformComponentMatrix>( 329 contentTransform * asyncUntransform * contentUntransform); 330 } 331 transform = transform * compensation; 332 333 return transform; 334 } 335 336 LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb( 337 const LayerToParentLayerMatrix4x4& aCurrentTransform, 338 const gfx::Matrix4x4& aScrollableContentTransform, 339 AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics, 340 const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant) { 341 return AsyncScrollThumbTransformer{ 342 aCurrentTransform, aScrollableContentTransform, aApzc, aMetrics, 343 aScrollbarData, aScrollbarIsDescendant} 344 .ComputeTransform(); 345 } 346 347 } // namespace apz 348 } // namespace layers 349 } // namespace mozilla