Bug 1356705 - Use CSS smooth scroll when smooth scrolling a XUL scrollbox. r?mconley draft
authorDão Gottwald <dao+bmo@mozilla.com>
Wed, 02 Aug 2017 16:50:02 -0400
changeset 619931 6913e1e2937f989c960ad17de1384583d04841c1
parent 619930 1fe3f4caf2b5a998874fe295021795a9de5f2bcf
child 640544 f1a6f6d85fbd6e59bb3ad957edb7ceae27d57fac
push id71871
push usermconley@mozilla.com
push dateWed, 02 Aug 2017 21:18:07 +0000
reviewersmconley
bugs1356705
milestone57.0a1
Bug 1356705 - Use CSS smooth scroll when smooth scrolling a XUL scrollbox. r?mconley MozReview-Commit-ID: 4Cjr1MuSVkk
browser/base/content/tabbrowser.xml
toolkit/content/widgets/scrollbox.xml
toolkit/content/xul.css
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -6634,19 +6634,19 @@
             // Can we make both the new tab and the selected tab completely visible?
             if (!selected ||
                 Math.max(tab.right - selected.left, selected.right - tab.left) <=
                   scrollRect.width) {
               this.mTabstrip.ensureElementIsVisible(aTab);
               return;
             }
 
-            this.mTabstrip._smoothScrollByPixels(this.mTabstrip._isRTLScrollbox ?
-                                                 selected.right - scrollRect.right :
-                                                 selected.left - scrollRect.left);
+            this.mTabstrip.scrollByPixels(this.mTabstrip._isRTLScrollbox ?
+                                          selected.right - scrollRect.right :
+                                          selected.left - scrollRect.left);
           }
 
           if (!this._animateElement.hasAttribute("highlight")) {
             this._animateElement.setAttribute("highlight", "true");
             setTimeout(function(ele) {
               ele.removeAttribute("highlight");
             }, 150, this._animateElement);
           }
--- a/toolkit/content/widgets/scrollbox.xml
+++ b/toolkit/content/widgets/scrollbox.xml
@@ -38,37 +38,38 @@
                             anonid="scrollbutton-up"
                             xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                             oncommand="_autorepeatbuttonScroll(event);"/>
       <xul:spacer class="arrowscrollbox-overflow-start-indicator"
                   xbl:inherits="collapsed=scrolledtostart"/>
       <xul:scrollbox class="arrowscrollbox-scrollbox"
                      anonid="scrollbox"
                      flex="1"
-                     xbl:inherits="orient,align,pack,dir">
+                     xbl:inherits="orient,align,pack,dir,smoothscroll">
         <children/>
       </xul:scrollbox>
       <xul:spacer class="arrowscrollbox-overflow-end-indicator"
                   xbl:inherits="collapsed=scrolledtoend"/>
       <xul:autorepeatbutton class="autorepeatbutton-down"
                             anonid="scrollbutton-down"
                             xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                             oncommand="_autorepeatbuttonScroll(event);"/>
     </content>
 
     <implementation>
       <constructor><![CDATA[
+        if (!this.hasAttribute("smoothscroll")) {
+          this.smoothScroll = this._prefBranch
+                                  .getBoolPref("toolkit.scrollbox.smoothScroll", true);
+        }
+
         this.setAttribute("notoverflowing", "true");
         this._updateScrollButtonsDisabledState();
       ]]></constructor>
 
-      <destructor><![CDATA[
-        this._stopSmoothScroll();
-      ]]></destructor>
-
       <field name="_scrollbox">
         document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
       </field>
       <field name="_scrollButtonUp">
         document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-up");
       </field>
       <field name="_scrollButtonDown">
         document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-down");
@@ -91,31 +92,22 @@
           if (this._scrollIncrement === null) {
             this._scrollIncrement = this._prefBranch
                                         .getIntPref("toolkit.scrollbox.scrollIncrement", 20);
           }
           return this._scrollIncrement;
         ]]></getter>
       </property>
 
-      <field name="_smoothScroll">null</field>
       <property name="smoothScroll">
         <getter><![CDATA[
-          if (this._smoothScroll === null) {
-            if (this.hasAttribute("smoothscroll")) {
-              this._smoothScroll = (this.getAttribute("smoothscroll") == "true");
-            } else {
-              this._smoothScroll = this._prefBranch
-                                       .getBoolPref("toolkit.scrollbox.smoothScroll", true);
-            }
-          }
-          return this._smoothScroll;
+          return this.getAttribute("smoothscroll") == "true";
         ]]></getter>
         <setter><![CDATA[
-          this._smoothScroll = val;
+          this.setAttribute("smoothscroll", !!val);
           return val;
         ]]></setter>
       </property>
 
       <field name="_scrollBoxObject">null</field>
       <property name="scrollBoxObject" readonly="true">
         <getter><![CDATA[
           if (!this._scrollBoxObject) {
@@ -249,187 +241,20 @@
 
       <method name="ensureElementIsVisible">
         <parameter name="element"/>
         <parameter name="aSmoothScroll"/>
         <body><![CDATA[
           if (!this._canScrollToElement(element))
             return;
 
-          var vertical = this.orient == "vertical";
-          var rect = this.scrollClientRect;
-          var containerStart = vertical ? rect.top : rect.left;
-          var containerEnd = vertical ? rect.bottom : rect.right;
-          rect = element.getBoundingClientRect();
-          var elementStart = vertical ? rect.top : rect.left;
-          var elementEnd = vertical ? rect.bottom : rect.right;
-
-          var scrollPaddingRect = this.scrollPaddingRect;
-          let style = window.getComputedStyle(this._scrollbox);
-          var scrollContentRect = {
-            left: scrollPaddingRect.left + parseFloat(style.paddingLeft),
-            top: scrollPaddingRect.top + parseFloat(style.paddingTop),
-            right: scrollPaddingRect.right - parseFloat(style.paddingRight),
-            bottom: scrollPaddingRect.bottom - parseFloat(style.paddingBottom)
-          };
-
-          if (elementStart <= (vertical ? scrollContentRect.top : scrollContentRect.left)) {
-            elementStart = vertical ? scrollPaddingRect.top : scrollPaddingRect.left;
-          }
-          if (elementEnd >= (vertical ? scrollContentRect.bottom : scrollContentRect.right)) {
-            elementEnd = vertical ? scrollPaddingRect.bottom : scrollPaddingRect.right;
-          }
-
-          var amountToScroll;
-
-          if (elementStart < containerStart) {
-            amountToScroll = elementStart - containerStart;
-          } else if (containerEnd < elementEnd) {
-            amountToScroll = elementEnd - containerEnd;
-          } else if (this._isScrolling) {
-            // decelerate if a currently-visible element is selected during the scroll
-            const STOP_DISTANCE = 15;
-            if (this._isScrolling == -1 && elementStart - STOP_DISTANCE < containerStart)
-              amountToScroll = elementStart - containerStart;
-            else if (this._isScrolling == 1 && containerEnd - STOP_DISTANCE < elementEnd)
-              amountToScroll = elementEnd - containerEnd;
-            else
-              amountToScroll = this._isScrolling * STOP_DISTANCE;
-          } else {
-            return;
-          }
-
-          this._stopSmoothScroll();
-
-          if (aSmoothScroll != false && this.smoothScroll) {
-            this._smoothScrollByPixels(amountToScroll, element);
-          } else {
-            this.scrollByPixels(amountToScroll);
-          }
-        ]]></body>
-      </method>
-
-      <method name="_smoothScrollByPixels">
-        <parameter name="amountToScroll"/>
-        <parameter name="element"/><!-- optional -->
-        <body><![CDATA[
-          if (amountToScroll == 0)
-            return;
-
-          // Shouldn't forget pending scroll amount if the scroll direction
-          // isn't changed because this may be called high frequency with very
-          // small pixel values.
-          var scrollDirection = 0;
-          if (amountToScroll) {
-            // Positive amountToScroll makes us scroll right (elements fly left),
-            // negative scrolls left.
-            scrollDirection = amountToScroll < 0 ? -1 : 1;
-          }
-
-          // However, if the scroll direction is changed, let's cancel the
-          // pending scroll because user must want to scroll from current
-          // position.
-          if (this._isScrolling && this._isScrolling != scrollDirection)
-            this._stopSmoothScroll();
-
-          this._scrollTarget = element;
-          this._isScrolling = scrollDirection;
-
-          this._scrollAnim.start(amountToScroll, !this._scrollTarget);
+          element.scrollIntoView({ behavior: aSmoothScroll == false ? "instant" : "auto" });
         ]]></body>
       </method>
 
-      <field name="_scrollAnim"><![CDATA[({
-        scrollbox: this,
-        distance: 0.0,
-        requestHandle: 0, /* 0 indicates there is no pending request */
-
-        // Be aware, |distance| may be dounble. I.e., the absolute value of it can
-        // be less than 1.  Set |isContinuousScroll| to true when the scroll may be
-        // a part of continous scroll, for example, it's caused by turning mosue wheel.
-        start: function scrollAnim_start(distance, isContinuousScroll) {
-          // When it's a continous scroll and the scroll was started, this needs to
-          // respect preceding scroll requests.  For example, 1.5px scroll occurs 2 times,
-          // 3px should be scrolled.  So, fractional values shouldn't be discarded.
-          if (isContinuousScroll && this.distance) {
-            // |this.startPos| is integer due to cache of |.scrollPosition|.  Therefore,
-            // we need to manage actual destination with |this.destination|.
-            var oldDestination = this.destination;
-            this.destination = this._clampPosition(this.destination + distance);
-
-            // If scroll position has already reached the ends, we need to do nothing.
-            if (oldDestination == this.destination)
-              return;
-
-            // If the integer part of the destination isn't changed, we need to do
-            // nothing now, wait next event.
-            if (Math.trunc(this.destination) == Math.trunc(this.destination - distance))
-              return;
-
-            // Let's restart animation from current position to the new destination.
-            if (this.requestHandle) {
-              this.stop();
-              this.startPos = this.scrollbox.scrollPosition;
-              // The call of |.stop()| causes clearing |this.distance| but let's recover it
-              // for keeping continuous scroll.
-              this.distance = this.destination - this.startPos;
-            }
-          } else {
-            this.startPos = this.scrollbox.scrollPosition;
-            this.destination = this._clampPosition(this.startPos + distance);
-            this.distance = this.destination - this.startPos;
-
-            // If absolute value of |this.distance| is less than 1px and this call is
-            // start of a continous scroll, should wait to scroll until accumulated
-            // scroll amount becomes 1px or greater.
-            if (isContinuousScroll && Math.abs(this.distance) < 1)
-              return;
-          }
-          this.duration = Math.min(1000, Math.round(50 * Math.sqrt(Math.abs(distance))));
-          this.startTime = window.performance.now();
-
-          if (!this.requestHandle)
-            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
-        },
-
-        stop: function scrollAnim_stop() {
-          window.cancelAnimationFrame(this.requestHandle);
-          this.requestHandle = 0;
-          // Reset continouos scroll transaction at stopping the scroll animation.
-          this.distance = 0;
-        },
-
-        sample: function scrollAnim_handleEvent(timeStamp) {
-          // Note that timeStamp sometimes older than start time.  If we use
-          // native value below, it causes scrolling revese direction.
-          // So, if the timeStamp is older, let's treat it as same as the start time.
-          const timePassed = Math.max(0, timeStamp - this.startTime);
-          const pos = timePassed >= this.duration ? 1 :
-                      1 - Math.pow(1 - timePassed / this.duration, 4);
-
-          this.scrollbox.scrollPosition = this.startPos + (this.distance * pos);
-
-          if (pos == 1)
-            this.scrollbox._stopSmoothScroll();
-          else
-            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
-        },
-
-        _clampPosition: function scrollAnim_clampPosition(aScrollPosition) {
-          if (aScrollPosition < 0) {
-            return 0;
-          }
-          var maxPos = this.scrollbox.scrollSize - this.scrollbox.scrollClientSize;
-          if (aScrollPosition > maxPos) {
-            return maxPos;
-          }
-          return aScrollPosition;
-        }
-      })]]></field>
-
       <method name="scrollByIndex">
         <parameter name="index"/>
         <parameter name="aSmoothScroll"/>
         <body><![CDATA[
           if (index == 0)
             return;
 
           // Each scrollByIndex call is expected to scroll the given number of
@@ -600,34 +425,20 @@
 
       <method name="scrollByPixels">
         <parameter name="px"/>
         <body><![CDATA[
           this.scrollPosition += px;
         ]]></body>
       </method>
 
-      <!-- 0: idle
-           1: scrolling right
-          -1: scrolling left -->
-      <field name="_isScrolling">0</field>
       <field name="_prevMouseScrolls">[null, null]</field>
 
       <field name="_touchStart">-1</field>
 
-      <method name="_stopSmoothScroll">
-        <body><![CDATA[
-          if (this._isScrolling) {
-            this._scrollAnim.stop();
-            this._isScrolling = 0;
-            this._scrollTarget = null;
-          }
-        ]]></body>
-      </method>
-
       <field name="_scrollButtonUpdatePending">false</field>
       <method name="_updateScrollButtonsDisabledState">
         <body><![CDATA[
           if (this.hasAttribute("notoverflowing")) {
             this.setAttribute("scrolledtoend", "true");
             this.setAttribute("scrolledtostart", "true");
             return;
           }
@@ -686,17 +497,16 @@
           });
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="wheel"><![CDATA[
         let doScroll = false;
-        let useSmoothScroll = event.deltaMode != event.DOM_DELTA_PIXEL && this.smoothScroll;
         let scrollAmount = 0;
         if (this.orient == "vertical") {
           doScroll = true;
           if (event.deltaMode == event.DOM_DELTA_PIXEL)
             scrollAmount = event.deltaY;
           else if (event.deltaMode == event.DOM_DELTA_PAGE)
             scrollAmount = event.deltaY * this.scrollClientSize;
           else
@@ -725,20 +535,17 @@
           }
 
           if (this._prevMouseScrolls.length > 1)
             this._prevMouseScrolls.shift();
           this._prevMouseScrolls.push(isVertical);
         }
 
         if (doScroll) {
-          if (useSmoothScroll)
-            this._smoothScrollByPixels(scrollAmount);
-          else
-            this.scrollByPixels(scrollAmount);
+          this.scrollByPixels(scrollAmount);
         }
 
         event.stopPropagation();
         event.preventDefault();
       ]]></handler>
 
       <handler event="touchstart"><![CDATA[
         if (event.touches.length > 1) {
@@ -857,17 +664,17 @@
                          onmouseup="if (event.button == 0) _stopScroll();"
                          onmouseover="_continueScroll(-1);"
                          onmouseout="_pauseScroll();"/>
       <xul:spacer class="arrowscrollbox-overflow-start-indicator"
                   xbl:inherits="collapsed=scrolledtostart"/>
       <xul:scrollbox class="arrowscrollbox-scrollbox"
                      anonid="scrollbox"
                      flex="1"
-                     xbl:inherits="orient,align,pack,dir">
+                     xbl:inherits="orient,align,pack,dir,smoothscroll">
         <children/>
       </xul:scrollbox>
       <xul:spacer class="arrowscrollbox-overflow-end-indicator"
                   xbl:inherits="collapsed=scrolledtoend"/>
       <xul:toolbarbutton class="scrollbutton-down"
                          xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                          anonid="scrollbutton-down"
                          onclick="_distanceScroll(event);"
@@ -901,51 +708,23 @@
           if (!document)
             aTimer.cancel();
 
           this.scrollByIndex(this._scrollIndex);
         ]]>
         </body>
       </method>
 
-      <field name="_arrowScrollAnim"><![CDATA[({
-        scrollbox: this,
-        requestHandle: 0, /* 0 indicates there is no pending request */
-        start: function arrowSmoothScroll_start() {
-          this.lastFrameTime = window.performance.now();
-          if (!this.requestHandle)
-            this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
-        },
-        stop: function arrowSmoothScroll_stop() {
-          window.cancelAnimationFrame(this.requestHandle);
-          this.requestHandle = 0;
-        },
-        sample: function arrowSmoothScroll_handleEvent(timeStamp) {
-          const scrollIndex = this.scrollbox._scrollIndex;
-          const timePassed = timeStamp - this.lastFrameTime;
-          this.lastFrameTime = timeStamp;
-
-          const scrollDelta = 0.5 * timePassed * scrollIndex;
-          this.scrollbox.scrollPosition += scrollDelta;
-
-          this.requestHandle = window.requestAnimationFrame(this.sample.bind(this));
-        }
-      })]]></field>
-
       <method name="_startScroll">
         <parameter name="index"/>
         <body><![CDATA[
           if (this._isRTLScrollbox)
             index *= -1;
           this._scrollIndex = index;
           this._mousedown = true;
-          if (this.smoothScroll) {
-            this._arrowScrollAnim.start();
-            return;
-          }
 
           if (!this._scrollTimer)
             this._scrollTimer =
               Components.classes["@mozilla.org/timer;1"]
                         .createInstance(Components.interfaces.nsITimer);
           else
             this._scrollTimer.cancel();
 
@@ -956,22 +735,17 @@
         </body>
       </method>
 
       <method name="_stopScroll">
         <body><![CDATA[
           if (this._scrollTimer)
             this._scrollTimer.cancel();
           this._mousedown = false;
-          if (!this._scrollIndex || !this.smoothScroll)
-            return;
-
-          this.scrollByIndex(this._scrollIndex);
           this._scrollIndex = 0;
-          this._arrowScrollAnim.stop();
         ]]></body>
       </method>
 
       <method name="_pauseScroll">
         <body><![CDATA[
           if (this._mousedown) {
             this._stopScroll();
             this._mousedown = true;
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -1068,16 +1068,20 @@ slider {
 /******** scrollbox ********/
 
 scrollbox {
   -moz-binding: url("chrome://global/content/bindings/scrollbox.xml#scrollbox");
   /* This makes it scrollable! */
   overflow: hidden;
 }
 
+scrollbox[smoothscroll=true] {
+  scroll-behavior: smooth;
+}
+
 arrowscrollbox {
   -moz-binding: url("chrome://global/content/bindings/scrollbox.xml#arrowscrollbox");
 }
 
 arrowscrollbox[clicktoscroll="true"] {
   -moz-binding: url("chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll");
 }