<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->


<bindings id="arrowscrollboxBindings"
   xmlns="http://www.mozilla.org/xbl"
   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="scrollbox-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
    <resources>
      <stylesheet src="chrome://global/skin/scrollbox.css"/>
    </resources>
  </binding>

  <binding id="scrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content>
      <xul:box class="box-inherit scrollbox-innerbox" xbl:inherits="orient,align,pack,dir" flex="1">
        <children/>
      </xul:box>
    </content>
  </binding>

  <binding id="arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content>
      <xul:autorepeatbutton class="autorepeatbutton-up"
                            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">
        <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[
        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");
      </field>

      <field name="__prefBranch">null</field>
      <property name="_prefBranch" readonly="true">
        <getter><![CDATA[
          if (this.__prefBranch === null) {
            this.__prefBranch = Components.classes['@mozilla.org/preferences-service;1']
                                          .getService(Components.interfaces.nsIPrefBranch);
          }
          return this.__prefBranch;
        ]]></getter>
      </property>

      <field name="_scrollIncrement">null</field>
      <property name="scrollIncrement" readonly="true">
        <getter><![CDATA[
          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;
        ]]></getter>
        <setter><![CDATA[
          this._smoothScroll = val;
          return val;
        ]]></setter>
      </property>

      <field name="_scrollBoxObject">null</field>
      <property name="scrollBoxObject" readonly="true">
        <getter><![CDATA[
          if (!this._scrollBoxObject) {
            this._scrollBoxObject = this._scrollbox.boxObject;
          }
          return this._scrollBoxObject;
        ]]></getter>
      </property>

      <property name="scrollClientRect" readonly="true">
        <getter><![CDATA[
          return this._scrollbox.getBoundingClientRect();
        ]]></getter>
      </property>

      <property name="scrollClientSize" readonly="true">
        <getter><![CDATA[
          return this.orient == "vertical" ?
                 this._scrollbox.clientHeight :
                 this._scrollbox.clientWidth;
        ]]></getter>
      </property>

      <property name="scrollSize" readonly="true">
        <getter><![CDATA[
          return this.orient == "vertical" ?
                 this._scrollbox.scrollHeight :
                 this._scrollbox.scrollWidth;
        ]]></getter>
      </property>
      <property name="scrollPaddingRect" readonly="true">
        <getter><![CDATA[
          // This assumes that this._scrollbox doesn't have any border.
          var outerRect = this.scrollClientRect;
          var innerRect = {};
          innerRect.left = outerRect.left - this._scrollbox.scrollLeft;
          innerRect.top = outerRect.top - this._scrollbox.scrollTop;
          innerRect.right = innerRect.left + this._scrollbox.scrollWidth;
          innerRect.bottom = innerRect.top + this._scrollbox.scrollHeight;
          return innerRect;
        ]]></getter>
      </property>
      <property name="scrollboxPaddingStart" readonly="true">
        <getter><![CDATA[
          var ltr = (window.getComputedStyle(this, null).direction == "ltr");
          var paddingStartName = ltr ? "padding-left" : "padding-right";
          var scrollboxStyle = window.getComputedStyle(this._scrollbox, null);
          return parseFloat(scrollboxStyle.getPropertyValue(paddingStartName));
        ]]></getter>
      </property>
      <property name="scrollPosition">
        <getter><![CDATA[
          return this.orient == "vertical" ?
                 this._scrollbox.scrollTop :
                 this._scrollbox.scrollLeft;
        ]]></getter>
        <setter><![CDATA[
          if (this.orient == "vertical")
            this._scrollbox.scrollTop = val;
          else
            this._scrollbox.scrollLeft = val;
          return val;
        ]]></setter>
      </property>

      <property name="_startEndProps" readonly="true">
        <getter><![CDATA[
          return this.orient == "vertical" ?
                 ["top", "bottom"] : ["left", "right"];
        ]]></getter>
      </property>

      <field name="_isRTLScrollbox"><![CDATA[
        this.orient != "vertical" &&
        document.defaultView.getComputedStyle(this._scrollbox, "").direction == "rtl";
      ]]></field>

      <field name="_scrollTarget">null</field>

      <method name="_canScrollToElement">
        <parameter name="element"/>
        <body><![CDATA[
          return window.getComputedStyle(element).display != "none";
        ]]></body>
      </method>

      <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, null);
          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)
          };

          // Provide an entry point for derived bindings to adjust these values.
          if (this._adjustElementStartAndEnd) {
            [elementStart, elementEnd] =
              this._adjustElementStartAndEnd(element, elementStart, elementEnd);
          }

          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[
          this._stopSmoothScroll();
          if (amountToScroll == 0)
            return;

          this._scrollTarget = element;
          // Positive amountToScroll makes us scroll right (elements fly left), negative scrolls left.
          this._isScrolling = amountToScroll < 0 ? -1 : 1;

          this._scrollAnim.start(amountToScroll);
        ]]></body>
      </method>

      <field name="_scrollAnim"><![CDATA[({
        scrollbox: this,
        requestHandle: 0, /* 0 indicates there is no pending request */
        start: function scrollAnim_start(distance) {
          this.distance = distance;
          this.startPos = this.scrollbox.scrollPosition;
          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;
        },
        sample: function scrollAnim_handleEvent(timeStamp) {
          const timePassed = 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));
        }
      })]]></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
          // items. If a previous call is still in progress because of smooth
          // scrolling, we need to complete it before starting a new one.
          if (this._scrollTarget) {
            let elements = this._getScrollableElements();
            if (this._scrollTarget != elements[0] &&
                this._scrollTarget != elements[elements.length - 1])
              this.ensureElementIsVisible(this._scrollTarget, false);
          }

          var rect = this.scrollClientRect;
          var [start, end] = this._startEndProps;
          var x = index > 0 ? rect[end] + 1 : rect[start] - 1;
          var nextElement = this._elementFromPoint(x, index);
          if (!nextElement)
            return;

          var targetElement;
          if (this._isRTLScrollbox)
            index *= -1;
          while (index < 0 && nextElement) {
            if (this._canScrollToElement(nextElement))
              targetElement = nextElement;
            nextElement = nextElement.previousSibling;
            index++;
          }
          while (index > 0 && nextElement) {
            if (this._canScrollToElement(nextElement))
              targetElement = nextElement;
            nextElement = nextElement.nextSibling;
            index--;
          }
          if (!targetElement)
            return;

          this.ensureElementIsVisible(targetElement, aSmoothScroll);
        ]]></body>
      </method>

      <method name="scrollByPage">
        <parameter name="pageDelta"/>
        <parameter name="aSmoothScroll"/>
        <body><![CDATA[
          if (pageDelta == 0)
            return;

          // If a previous call is still in progress because of smooth
          // scrolling, we need to complete it before starting a new one.
          if (this._scrollTarget) {
            let elements = this._getScrollableElements();
            if (this._scrollTarget != elements[0] &&
                this._scrollTarget != elements[elements.length - 1])
              this.ensureElementIsVisible(this._scrollTarget, false);
          }

          var [start, end] = this._startEndProps;
          var rect = this.scrollClientRect;
          var containerEdge = pageDelta > 0 ? rect[end] + 1 : rect[start] - 1;
          var pixelDelta = pageDelta * (rect[end] - rect[start]);
          var destinationPosition = containerEdge + pixelDelta;
          var nextElement = this._elementFromPoint(containerEdge, pageDelta);
          if (!nextElement)
            return;

          // We need to iterate over our elements in the direction of pageDelta.
          // pageDelta is the physical direction, so in a horizontal scroll box,
          // positive values scroll to the right no matter if the scrollbox is
          // LTR or RTL. But RTL changes how we need to advance the iteration
          // (whether to get the next or the previous sibling of the current
          // element).
          var logicalAdvanceDir = pageDelta * (this._isRTLScrollbox ? -1 : 1);
          var advance = logicalAdvanceDir > 0 ? (e => e.nextSibling) : (e => e.previousSibling);

          var extendsPastTarget = (pageDelta > 0)
            ? (e => e.getBoundingClientRect()[end] > destinationPosition)
            : (e => e.getBoundingClientRect()[start] < destinationPosition);

          // We want to scroll to the last element we encounter before we find
          // an element which extends past destinationPosition.
          var targetElement;
          do {
            if (this._canScrollToElement(nextElement))
              targetElement = nextElement;
            nextElement = advance(nextElement);
          } while (nextElement && !extendsPastTarget(nextElement));

          if (!targetElement)
            return;

          this.ensureElementIsVisible(targetElement, aSmoothScroll);
        ]]></body>
      </method>

      <method name="_getScrollableElements">
        <body><![CDATA[
          var nodes = this.childNodes;
          if (nodes.length == 1 &&
              nodes[0].localName == "children" &&
              nodes[0].namespaceURI == "http://www.mozilla.org/xbl") {
            nodes = document.getBindingParent(this).childNodes;
          }

          return Array.filter(nodes, this._canScrollToElement, this);
        ]]></body>
      </method>

      <method name="_elementFromPoint">
        <parameter name="aX"/>
        <parameter name="aPhysicalScrollDir"/>
        <body><![CDATA[
          var elements = this._getScrollableElements();
          if (!elements.length)
            return null;

          if (this._isRTLScrollbox)
            elements.reverse();

          var [start, end] = this._startEndProps;
          var low = 0;
          var high = elements.length - 1;

          if (aX < elements[low].getBoundingClientRect()[start] ||
              aX > elements[high].getBoundingClientRect()[end])
            return null;

          var mid, rect;
          while (low <= high) {
            mid = Math.floor((low + high) / 2);
            rect = elements[mid].getBoundingClientRect();
            if (rect[start] > aX)
              high = mid - 1;
            else if (rect[end] < aX)
              low = mid + 1;
            else
              return elements[mid];
          }

          // There's no element at the requested coordinate, but the algorithm
          // from above yields an element next to it, in a random direction.
          // The desired scrolling direction leads to the correct element.

          if (!aPhysicalScrollDir)
            return null;

          if (aPhysicalScrollDir < 0 && rect[start] > aX)
            mid = Math.max(mid - 1, 0);
          else if (aPhysicalScrollDir > 0 && rect[end] < aX)
            mid = Math.min(mid + 1, elements.length - 1);

          return elements[mid];
        ]]></body>
      </method>

      <method name="_autorepeatbuttonScroll">
        <parameter name="event"/>
        <body><![CDATA[
          var dir = event.originalTarget == this._scrollButtonUp ? -1 : 1;
          if (this._isRTLScrollbox)
            dir *= -1;

          this.scrollByPixels(this.scrollIncrement * dir);

          event.stopPropagation();
        ]]></body>
      </method>

      <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>

      <method name="_updateScrollButtonsDisabledState">
        <body><![CDATA[
          var scrolledToStart = false;
          var scrolledToEnd = false;

          if (this.hasAttribute("notoverflowing")) {
            scrolledToStart = true;
            scrolledToEnd = true;
          }
          else if (this.scrollPosition == 0) {
            // In the RTL case, this means the _last_ element in the
            // scrollbox is visible
            if (this._isRTLScrollbox)
              scrolledToEnd = true;
            else
              scrolledToStart = true;
          }
          else if (this.scrollClientSize + this.scrollPosition == this.scrollSize) {
            // In the RTL case, this means the _first_ element in the
            // scrollbox is visible
            if (this._isRTLScrollbox)
              scrolledToStart = true;
            else
              scrolledToEnd = true;
          }

          if (scrolledToEnd)
            this.setAttribute("scrolledtoend", "true");
          else
            this.removeAttribute("scrolledtoend");

          if (scrolledToStart)
            this.setAttribute("scrolledtostart", "true");
          else
            this.removeAttribute("scrolledtostart");
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="wheel"><![CDATA[
        if (this.orient == "vertical") {
          if (event.deltaMode == event.DOM_DELTA_PIXEL)
            this.scrollByPixels(event.deltaY);
          else if (event.deltaMode == event.DOM_DELTA_PAGE)
            this.scrollByPage(event.deltaY);
          else
            this.scrollByIndex(event.deltaY);
        }
        // We allow vertical scrolling to scroll a horizontal scrollbox
        // because many users have a vertical scroll wheel but no
        // horizontal support.
        // Because of this, we need to avoid scrolling chaos on trackpads
        // and mouse wheels that support simultaneous scrolling in both axes.
        // We do this by scrolling only when the last two scroll events were
        // on the same axis as the current scroll event.
        // For diagonal scroll events we only respect the dominant axis.
        else {
          let isVertical = Math.abs(event.deltaY) > Math.abs(event.deltaX);
          let delta = isVertical ? event.deltaY : event.deltaX;
          let scrollByDelta = isVertical && this._isRTLScrollbox ? -delta : delta;

          if (this._prevMouseScrolls.every(prev => prev == isVertical)) {
            if (event.deltaMode == event.DOM_DELTA_PIXEL)
              this.scrollByPixels(scrollByDelta);
            else if (event.deltaMode == event.DOM_DELTA_PAGE)
              this.scrollByPage(scrollByDelta);
            else
              this.scrollByIndex(scrollByDelta);
          }

          if (this._prevMouseScrolls.length > 1)
            this._prevMouseScrolls.shift();
          this._prevMouseScrolls.push(isVertical);
        }

        event.stopPropagation();
        event.preventDefault();
      ]]></handler>

      <handler event="touchstart"><![CDATA[
        if (event.touches.length > 1) {
          // Multiple touch points detected, abort. In particular this aborts
          // the panning gesture when the user puts a second finger down after
          // already panning with one finger. Aborting at this point prevents
          // the pan gesture from being resumed until all fingers are lifted
          // (as opposed to when the user is back down to one finger).
          this._touchStart = -1;
        } else {
          this._touchStart = (this.orient == "vertical"
                ? event.touches[0].screenY
                : event.touches[0].screenX);
        }
      ]]></handler>

      <handler event="touchmove"><![CDATA[
        if (event.touches.length == 1 &&
            this._touchStart >= 0) {
          var touchPoint = (this.orient == "vertical"
                ? event.touches[0].screenY
                : event.touches[0].screenX);
          var delta = this._touchStart - touchPoint;
          if (Math.abs(delta) > 0) {
            this.scrollByPixels(delta);
            this._touchStart = touchPoint;
          }
          event.preventDefault();
        }
      ]]></handler>

      <handler event="touchend"><![CDATA[
        this._touchStart = -1;
      ]]></handler>

      <handler event="underflow" phase="capturing"><![CDATA[
        // filter underflow events which were dispatched on nested scrollboxes
        if (event.target != this)
          return;

        // Ignore events that doesn't match our orientation.
        // Scrollport event orientation:
        //   0: vertical
        //   1: horizontal
        //   2: both
        if (this.orient == "vertical") {
          if (event.detail == 1)
            return;
        }
        else if (event.detail == 0) {
          // horizontal scrollbox
          return;
        }

        this.setAttribute("notoverflowing", "true");

        try {
          // See bug 341047 and comments in overflow handler as to why
          // try..catch is needed here
          this._updateScrollButtonsDisabledState();

          let childNodes = this._getScrollableElements();
          if (childNodes && childNodes.length)
            this.ensureElementIsVisible(childNodes[0], false);
        }
        catch (e) {
          this.removeAttribute("notoverflowing");
        }
      ]]></handler>

      <handler event="overflow" phase="capturing"><![CDATA[
        // filter underflow events which were dispatched on nested scrollboxes
        if (event.target != this)
          return;

        // Ignore events that doesn't match our orientation.
        // Scrollport event orientation:
        //   0: vertical
        //   1: horizontal
        //   2: both
        if (this.orient == "vertical") {
          if (event.detail == 1)
            return;
        }
        else if (event.detail == 0) {
          // horizontal scrollbox
          return;
        }

        this.removeAttribute("notoverflowing");

        try {
          // See bug 341047, the overflow event is dispatched when the
          // scrollbox already is mostly destroyed. This causes some code in
          // _updateScrollButtonsDisabledState() to throw an error. It also
          // means that the notoverflowing attribute was removed erroneously,
          // as the whole overflow event should not be happening in that case.
          this._updateScrollButtonsDisabledState();
        }
        catch (e) {
          this.setAttribute("notoverflowing", "true");
        }
      ]]></handler>

      <handler event="scroll" action="this._updateScrollButtonsDisabledState()"/>
    </handlers>
  </binding>

  <binding id="autorepeatbutton" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content repeat="hover">
      <xul:image class="autorepeatbutton-icon"/>
    </content>
  </binding>

  <binding id="arrowscrollbox-clicktoscroll" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox">
    <content>
      <xul:toolbarbutton class="scrollbutton-up"
                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                         anonid="scrollbutton-up"
                         onclick="_distanceScroll(event);"
                         onmousedown="if (event.button == 0) _startScroll(-1);"
                         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">
        <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);"
                         onmousedown="if (event.button == 0) _startScroll(1);"
                         onmouseup="if (event.button == 0) _stopScroll();"
                         onmouseover="_continueScroll(1);"
                         onmouseout="_pauseScroll();"/>
    </content>
    <implementation implements="nsITimerCallback, nsIDOMEventListener">
      <constructor><![CDATA[
        this._scrollDelay = this._prefBranch
                                .getIntPref("toolkit.scrollbox.clickToScroll.scrollDelay",
                                            this._scrollDelay);
      ]]></constructor>

      <destructor><![CDATA[
        // Release timer to avoid reference cycles.
        if (this._scrollTimer) {
          this._scrollTimer.cancel();
          this._scrollTimer = null;
        }
      ]]></destructor>

      <field name="_scrollIndex">0</field>
      <field name="_scrollDelay">150</field>

      <method name="notify">
        <parameter name="aTimer"/>
        <body>
        <![CDATA[
          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();

          this._scrollTimer.initWithCallback(this, this._scrollDelay,
                                             this._scrollTimer.TYPE_REPEATING_SLACK);
          this.notify(this._scrollTimer);
        ]]>
        </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;
            document.addEventListener("mouseup", this, false);
            document.addEventListener("blur", this, true);
          }
        ]]></body>
      </method>

      <method name="_continueScroll">
        <parameter name="index"/>
        <body><![CDATA[
          if (this._mousedown)
            this._startScroll(index);
        ]]></body>
      </method>

      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.type == "mouseup" ||
              aEvent.type == "blur" && aEvent.target == document) {
            this._mousedown = false;
            document.removeEventListener("mouseup", this, false);
            document.removeEventListener("blur", this, true);
          }
        ]]></body>
      </method>

      <method name="_distanceScroll">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.detail < 2 || aEvent.detail > 3)
            return;

          var scrollBack = (aEvent.originalTarget == this._scrollButtonUp);
          var scrollLeftOrUp = this._isRTLScrollbox ? !scrollBack : scrollBack;
          var targetElement;

          if (aEvent.detail == 2) {
            // scroll by the size of the scrollbox
            let [start, end] = this._startEndProps;
            let x;
            if (scrollLeftOrUp)
              x = this.scrollClientRect[start] - this.scrollClientSize;
            else
              x = this.scrollClientRect[end] + this.scrollClientSize;
            targetElement = this._elementFromPoint(x, scrollLeftOrUp ? -1 : 1);

            // the next partly-hidden element will become fully visible,
            // so don't scroll too far
            if (targetElement)
              targetElement = scrollBack ?
                              targetElement.nextSibling :
                              targetElement.previousSibling;
          }

          if (!targetElement) {
            // scroll to the first resp. last element
            let elements = this._getScrollableElements();
            targetElement = scrollBack ?
                            elements[0] :
                            elements[elements.length - 1];
          }

          this.ensureElementIsVisible(targetElement);
        ]]></body>
      </method>

    </implementation>
  </binding>
</bindings>