Bug 485385. Refactor kinetic code. Patch from Ben Combee <combee@mozilla.com>. r/sr=me.
authorbcombee@bcombee-linux-vm
Thu, 02 Apr 2009 16:45:27 -0400
changeset 65155 34932ce7c9801e201b48aa4fd27cf06eb0ed46fb
parent 65154 35edef680c52863702744fa5df1603967e968cc2
child 65156 c16719e1d23009e6811bb1ab14e2e9c1bdfbfec0
push idunknown
push userunknown
push dateunknown
bugs485385
Bug 485385. Refactor kinetic code. Patch from Ben Combee <combee@mozilla.com>. r/sr=me.
mobile/chrome/content/InputHandler.js
mobile/chrome/content/WidgetStack.js
--- a/mobile/chrome/content/InputHandler.js
+++ b/mobile/chrome/content/InputHandler.js
@@ -166,81 +166,87 @@ function DragData(owner, dragRadius, dra
   this._dragStartTimeoutLength = dragStartTimeoutLength;
   this.dragStartTimeout = -1;
   this.reset();
 }
 
 DragData.prototype = {
   reset: function reset() {
     this.dragging = false;
-    this.sX = 0;
-    this.sY = 0;
+    this.sX = null;
+    this.sY = null;
+    this.alreadyLocked = false;
     this.lockedX = null;
     this.lockedY = null;
 
     this.clearDragStartTimeout();
   },
 
-  setDragStart: function setDragStart(screenX, screenY) {
+  setDragPosition: function setDragPosition(screenX, screenY) {
     this.sX = screenX;
     this.sY = screenY;
-    this.dragStartTimeout = setTimeout(
+  },
+
+  setDragStart: function setDragStart(screenX, screenY) {
+    this.setDragPosition(screenX, screenY);
+    this.dragStartTimeout = window.setTimeout(
       function(dragData, sX, sY) { dragData.clearDragStartTimeout(); dragData._owner._dragStart(sX, sY); },
       this._dragStartTimeoutLength,
       this, screenX, screenY);
   },
 
   clearDragStartTimeout: function clearDragStartTimeout() {
     if (this.dragStartTimeout != -1)
-      clearTimeout(this.dragStartTimeout);
+      window.clearTimeout(this.dragStartTimeout);
     this.dragStartTimeout = -1;
   },
 
-  setLockedAxis: function setLockedAxis(sX, sY) {
+  lockMouseMove: function lockMouseMove(sX, sY) {
+    if (this.lockedX !== null)
+      sX = this.lockedX;
+    else if (this.lockedY !== null)
+      sY = this.lockedY;
+    return [sX, sY];
+  },
+
+  lockAxis: function lockAxis(sX, sY) {
+    if (this.alreadyLocked)
+      return lockMouseMove(sX, sY);
+
     // look at difference from stored coord to lock movement, but only
     // do it if initial movement is sufficient to detect intent
     let absX = Math.abs(this.sX - sX);
     let absY = Math.abs(this.sY - sY);
 
     // lock panning if we move more than half of the drag radius and that direction
     // contributed more than 2/3rd to the radial movement
     if ((absX > (this._dragRadius / 2)) && ((absX * absX) > (2 * absY * absY))) {
       this.lockedY = this.sY;
       sY = this.sY;
     }
     else if ((absY > (this._dragRadius / 2)) && ((absY * absY) > (2 * absX * absX))) {
       this.lockedX = this.sX;
       sX = this.sX;
     }
-
-    return [sX, sY];
-  },
-
-  lockMouseMove: function lockMouseMove(sX, sY) {
-    if (this.lockedX !== null)
-      sX = this.lockedX;
-    else if (this.lockedY !== null)
-      sY = this.lockedY;
+    this.alreadyLocked = true;
 
     return [sX, sY];
   },
 
   /* returns true if we go ahead and start a drag */
   detectEarlyDrag: function detectEarlyDrag(sX, sY) {
     let dx = this.sX - sX;
     let dy = this.sY - sY;
 
     if (!this.dragging && this.dragStartTimeout != -1) {
       if ((dx*dx + dy*dy) > (this._dragRadius * this._dragRadius)) {
         this.clearDragStartTimeout();
         this._owner._dragStart(sX, sY);
-        return true;
       }
     }
-    return false;
   }
 };
 
 
 /**
  * Panning code for chrome elements
  */
 
@@ -285,17 +291,17 @@ ChromeInputModule.prototype = {
     this._dragData.reset();
     this._targetScrollbox = null;
   },
 
   _dragStart: function _dragStart(sX, sY) {
     let dragData = this._dragData;
     dragData.dragging = true;
 
-    [sX, sY] = dragData.setLockedAxis(sX, sY);
+    [sX, sY] = dragData.lockAxis(sX, sY);
 
     // grab all events until we stop the drag
     ws.dragStart(sX, sY);
 
     // prevent clicks from being sent once we start drag
     this._clickEvents = [];
   },
 
@@ -307,18 +313,17 @@ ChromeInputModule.prototype = {
     this._targetScrollbox = null;
   },
 
   _dragMove: function _dragMove(sX, sY) {
     let dragData = this._dragData;
     [sX, sY] = dragData.lockMouseMove(sX, sY);
     if (this._targetScrollbox)
       this._targetScrollbox.scrollBy(dragData.sX - sX, dragData.sY - sY);
-    dragData.sX = sX;
-    dragData.sY = sY;
+    this.setDragPosition(sX, sY);
   },
 
   _onMouseDown: function _onMouseDown(aEvent) {
     // exit early for events in the content area
     if (aEvent.target === this._browserCanvas) {
       return;
     }
 
@@ -330,16 +335,17 @@ ChromeInputModule.prototype = {
 
     // absorb the event for the scrollable XUL element and make all future events grabbed too
     this._owner.grab(this);
 
     aEvent.stopPropagation();
     aEvent.preventDefault();
 
     dragData.setDragStart(aEvent.screenX, aEvent.screenY);
+    this._onMouseMove(aEvent); // treat this as a mouse move too
 
     // store away the event for possible sending later if a drag doesn't happen
     let clickEvent = document.createEvent("MouseEvent");
     clickEvent.initMouseEvent(aEvent.type, aEvent.bubbles, aEvent.cancelable,
                               aEvent.view, aEvent.detail,
                               aEvent.screenX, aEvent.screenY, aEvent.clientX, aEvent.clientY,
                               aEvent.ctrlKey, aEvent.altKey, aEvent.shiftKeyArg, aEvent.metaKeyArg,
                               aEvent.button, aEvent.relatedTarget);
@@ -388,18 +394,22 @@ ChromeInputModule.prototype = {
       return;
 
     aEvent.stopPropagation();
     aEvent.preventDefault();
 
     let sX = aEvent.screenX;
     let sY = aEvent.screenY;
 
-    if (dragData.detectEarlyDrag(sX, sY))
-      return;
+    if (!dragData.sX)
+      dragData.setDragPosition(aEvent.screenX, aEvent.screenY);
+
+    let [sX, sY] = dragData.lockMouseMove(aEvent.screenX, aEvent.screenY);
+
+    dragData.detectEarlyDrag(sX, sY);
 
     if (!dragData.dragging)
       return;
 
     [sX, sY] = dragData.lockMouseMove(sX, sY);
     this._dragMove(sX, sY);
   },
 
@@ -432,51 +442,174 @@ ChromeInputModule.prototype = {
                        aEvent.button, aEvent.detail, 0, true);
   }
 };
 
 /**
  * Kinetic panning code for content
  */
 
-function KineticData() {
+function KineticData(owner) {
+  this._owner = owner;
   this.kineticHandle = -1;
   this.reset();
 }
 
 KineticData.prototype = {
   reset: function reset() {
     if (this.kineticHandle != -1) {
-      window.clearInterval(this.kineticHandle);
+      window.clearTimeout(this.kineticHandle);
       this.kineticHandle = -1;
     }
 
-    this.kineticStepSize = 15;
-    this.kineticDecelloration = 0.004;
+    this.stepSize = 30;
+    this.decelloration = 0.004;
     this.momentumBufferSize = 3;
     this.momentumBuffer = [];
     this.momentumBufferIndex = 0;
     this.lastTime = 0;
-    this.kineticDuration = 0;
-    this.kineticDirX = 0;
-    this.kineticDirY = 0;
-    this.kineticStep  = 0;
-    this.kineticStartX  = 0;
-    this.kineticStartY  = 0;
-    this.kineticInitialVel = 0;
+    this.duration = 0;
+    this.dirX = 0;
+    this.dirY = 0;
+    this.step  = 0;
+    this.stepStart = 0;
+    this.startX  = 0;
+    this.startY  = 0;
+    this.initialVel = 0;
+  },
+
+  startKinetic: function startKinetic(sX, sY) {
+    let dx = 0;
+    let dy = 0;
+    let dt = 0;
+    if (this.initialVel != 0)
+      return true;
+
+    if (!this.momentumBuffer)
+      return false;
+
+    for (let i = 0; i < this.momentumBufferSize; i++) {
+      let me = this.momentumBuffer[(this.momentumBufferIndex + i) % this.momentumBufferSize];
+      if (!me)
+        return false;
+
+      dx += me.dx;
+      dy += me.dy;
+      dt += me.dt;
+    }
+    if (dt <= 0)
+      return false;
+
+    let dist = Math.sqrt(dx*dx+dy*dy);
+    let vel  = dist/dt;
+    if (vel < 1)
+      return false;
+
+    this.dirX = dx/dist;
+    this.dirY = dy/dist;
+    if (this.dirX > 0.9) {
+      this.dirX = 1;
+      this.dirY = 0;
+    }
+    else if (this.dirY < -0.9) {
+      this.dirX = 0;
+      this.dirY = -1;
+    }
+    else if (this.dirX < -0.9) {
+      this.dirX = -1;
+      this.dirY = 0;
+    }
+    else if (this.dirY > 0.9) {
+      this.dirX = 0;
+      this.dirY = 1;
+    }
+
+    this.duration = vel/(2 * this.decelloration);
+    this.step = 0;
+    this.startX =  sX;
+    this.startY =  sY;
+    this.initialVel = vel;
+    this.stepStart = Date.now();
+    this.kineticHandle = window.setTimeout(this._doKinetic, this.stepSize, this);
+    return true;
+  },
+
+  _doKinetic: function _doKinetic(self) {
+    let t = self.step * self.stepSize;
+    let dt = Date.now() - self.stepStart - t; /* delta beween ideal activation time and now */
+
+    let extraSteps = Math.floor(dt / self.stepSize);
+    self.step += extraSteps;
+    t = self.step * self.stepSize;
+    dt -= extraSteps * self.stepSize;
+
+    if (t > self.duration)
+      t = self.duration;
+    let dist = self.initialVel * t -
+               self.decelloration * t * t;
+    let newX = Math.floor(self.dirX * dist + self.startX);
+    let newY = Math.floor(self.dirY * dist + self.startY);
+
+    let panned = self._owner._dragMove(newX, newY);
+    if(!panned || t >= self.duration) {
+      self.endKinetic(newX, newY);
+      return;
+    }
+
+    ++self.step;
+
+    /* setup the next iteration of this */
+    self.kineticHandle = window.setTimeout(self._doKinetic, self.stepSize - dt, self);
+  },
+
+  endKinetic: function endKinetic(sX, sY) {
+    ws.dragStop();
+    this.reset();
+
+    // Make sure that sidebars don't stay partially open
+    // XXX this should live somewhere else
+    let [leftVis,] = ws.getWidgetVisibility("tabs-container", false);
+    let [rightVis,] = ws.getWidgetVisibility("browser-controls", false);
+    if (leftVis != 0 && leftVis != 1) {
+      let w = document.getElementById("tabs-container").getBoundingClientRect().width;
+      if (leftVis >= 0.6666)
+        ws.panBy(-w, 0, true);
+      else
+        ws.panBy(leftVis * w, 0, true);
+    }
+    else if (rightVis != 0 && rightVis != 1) {
+      let w = document.getElementById("browser-controls").getBoundingClientRect().width;
+      if (rightVis >= 0.6666)
+        ws.panBy(w, 0, true);
+      else
+        ws.panBy(-rightVis * w, 0, true);
+    }
+  },
+
+  addData: function addData(dx, dy) {
+    if (dx == 0 && dy == 0)
+      return;
+
+    let t = Date.now();
+    let dt = this.lastTime ? t - this.lastTime : 0;
+
+    this.lastTime = t;
+    this.momentumBuffer[this.momentumBufferIndex] = { 'dx': dx, 'dy': dy, 'dt': dt };
+    this.momentumBufferIndex++;
+    this.momentumBufferIndex %= this.momentumBufferSize;
   }
 };
 
 function ContentPanningModule(owner, browserCanvas, useKinetic) {
   this._owner = owner;
   if (useKinetic !== undefined)
     this._useKinetic = useKinetic;
   this._browserCanvas = browserCanvas;
   this._dragData = new DragData(this, 20, 200);
-  this._kineticData = new KineticData();
+  this._kineticData = new KineticData(this);
 }
 
 ContentPanningModule.prototype = {
   _owner: null,
   _dragData: null,
 
   _useKinetic: true,
   _kineticData: null,
@@ -500,25 +633,26 @@ ContentPanningModule.prototype = {
   },
 
   /* If someone else grabs events ahead of us, cancel any pending
    * timeouts we may have.
    */
   cancelPending: function cancelPending() {
     let dragData = this._dragData;
     // stop scrolling, pass last coordinate we used
-    this._endKinetic(dragData.sX, dragData.sY);
+    this._kineticData.endKinetic(dragData.sX, dragData.sY);
+    this._owner.ungrab(this);
     dragData.reset();
   },
 
   _dragStart: function _dragStart(sX, sY) {
     let dragData = this._dragData;
     dragData.dragging = true;
 
-    [sX, sY] = dragData.setLockedAxis(sX, sY);
+    [sX, sY] = dragData.lockAxis(sX, sY);
 
     // grab all events until we stop the drag
     this._owner.grab(this);
     ws.dragStart(sX, sY);
 
     Browser.canvasBrowser.startPanning();
 
     // set the kinetic start time
@@ -529,188 +663,86 @@ ContentPanningModule.prototype = {
     let dragData = this._dragData;
 
     this._owner.ungrab(this);
 
     [sX, sY] = dragData.lockMouseMove(sX, sY);
 
     if (this._useKinetic) {
       // start kinetic scrolling here for canvas only
-      if (!this._startKinetic(sX, sY))
-        this._endKinetic(sX, sY);
+      if (!this._kineticData.startKinetic(sX, sY))
+        this._kineticData.endKinetic(sX, sY);
+      dragData.reset();
     }
     else {
       ws.dragStop();
+      // flush any paints that might be left so that our next pan will be fast
+      Browser.canvasBrowser.endPanning();
     }
-
-    // flush any paints that might be left so that our next pan will be fast
-    Browser.canvasBrowser.endPanning();
   },
 
   _dragMove: function _dragMove(sX, sY) {
+
     let dragData = this._dragData;
     [sX, sY] = dragData.lockMouseMove(sX, sY);
-    ws.dragMove(sX, sY);
-    dragData.sX = sX;
-    dragData.sY = sY;
+    let panned = ws.dragMove(sX, sY);
+    dragData.setDragPosition(sX, sY);
+    return panned;
   },
 
   _onMouseDown: function _onMouseDown(aEvent) {
     // if we're in the process of kineticly scrolling, stop and start over
-    if (this._kineticData.kineticHandle != -1)
-      this._endKinetic(aEvent.screenX, aEvent.screenY);
+    if (this._kineticData.kineticHandle != -1) {
+      this._kineticData.endKinetic(aEvent.screenX, aEvent.screenY);
+      this._owner.ungrab(this);
+      this._dragData.reset();
+    }
 
     this._dragData.setDragStart(aEvent.screenX, aEvent.screenY);
+    this._onMouseMove(aEvent); // treat this as a mouse move too
   },
 
   _onMouseUp: function _onMouseUp(aEvent) {
     let dragData = this._dragData;
 
-    if (dragData.dragging)
+    if (dragData.dragging) {
+      this._onMouseMove(aEvent); // treat this as a mouse move, incase our x/y are different
       this._dragStop(aEvent.screenX, aEvent.screenY);
+    }
 
     dragData.reset(); // be sure to reset the timer
   },
 
   _onMouseMove: function _onMouseMove(aEvent) {
     // don't do anything if we're in the process of kineticly scrolling
     if (this._kineticData.kineticHandle != -1)
       return;
 
     let dragData = this._dragData;
 
-    let sX = aEvent.screenX;
-    let sY = aEvent.screenY;
-
-    if (dragData.detectEarlyDrag(sX, sY))
-      return;
+    // if we never received a mouseDown, we need to go ahead and set this data
+    if (!dragData.sX)
+      dragData.setDragPosition(aEvent.screenX, aEvent.screenY);
 
-    if (!dragData.dragging)
-      return;
+    let [sX, sY] = dragData.lockMouseMove(aEvent.screenX, aEvent.screenY);
 
-    this._dragMove(sX, sY);
-
+    // even if we haven't started dragging yet, we should queue up the
+    // mousemoves in case we do start
     if (this._useKinetic) {
       // update our kinetic data
-      let kineticData = this._kineticData;
-      let t = Date.now();
       let dx = dragData.sX - sX;
       let dy = dragData.sY - sY;
-      let dt = t - kineticData.lastTime;
-      kineticData.lastTime = t;
-      let momentumBuffer = { dx: -dx, dy: -dy, dt: dt };
-
-      kineticData.momentumBuffer[kineticData.momentumBufferIndex] = momentumBuffer;
-      kineticData.momentumBufferIndex++;
-      kineticData.momentumBufferIndex %= kineticData.momentumBufferSize;
-    }
-  },
-
-  _startKinetic: function _startKinetic(sX, sY) {
-    let kineticData = this._kineticData;
-
-    let dx = 0;
-    let dy = 0;
-    let dt = 0;
-    if (kineticData.kineticInitialVel != 0)
-      return true;
-
-    if (!kineticData.momentumBuffer)
-      return false;
-
-    for (let i = 0; i < kineticData.momentumBufferSize; i++) {
-      let me = kineticData.momentumBuffer[(kineticData.momentumBufferIndex + i) % kineticData.momentumBufferSize];
-      if (!me)
-        return false;
-
-      dx += me.dx;
-      dy += me.dy;
-      dt += me.dt;
-    }
-    if (dt <= 0)
-      return false;
-
-    let dist = Math.sqrt(dx*dx+dy*dy);
-    let vel  = dist/dt;
-    if (vel < 1)
-      return false;
-
-    kineticData.kineticDirX = dx/dist;
-    kineticData.kineticDirY = dy/dist;
-    if (kineticData.kineticDirX > 0.9) {
-      kineticData.kineticDirX = 1;
-      kineticData.kineticDirY = 0;
-    }
-    else if (kineticData.kineticDirY < -0.9) {
-      kineticData.kineticDirX = 0;
-      kineticData.kineticDirY = -1;
-    }
-    else if (kineticData.kineticDirX < -0.9) {
-      kineticData.kineticDirX = -1;
-      kineticData.kineticDirY = 0;
-    }
-    else if (kineticData.kineticDirY > 0.9) {
-      kineticData.kineticDirX = 0;
-      kineticData.kineticDirY = 1;
+      this._kineticData.addData(-dx, -dy);
     }
 
-    kineticData.kineticDuration = vel/(2 * kineticData.kineticDecelloration);
-    kineticData.kineticStep = 0;
-    kineticData.kineticStartX =  sX;
-    kineticData.kineticStartY =  sY;
-    kineticData.kineticInitialVel = vel;
-    kineticData.kineticHandle = window.setInterval(this._doKinetic, kineticData.kineticStepSize, this);
-    return true;
-  },
-
-  _doKinetic: function _doKinetic(self) {
-    let kineticData = self._kineticData;
-
-    let t = kineticData.kineticStep * kineticData.kineticStepSize;
-    kineticData.kineticStep++;
-    if (t > kineticData.kineticDuration)
-      t = kineticData.kineticDuration;
-    let dist = kineticData.kineticInitialVel * t -
-               kineticData.kineticDecelloration * t * t;
-    let newX = Math.floor(kineticData.kineticDirX * dist + kineticData.kineticStartX);
-    let newY = Math.floor(kineticData.kineticDirY * dist + kineticData.kineticStartY);
-
-    self._dragMove(newX, newY);
-
-    if(t >= kineticData.kineticDuration)
-      self._endKinetic(newX, newY);
-  },
+    dragData.detectEarlyDrag(sX, sY);
 
-  _endKinetic: function _endKinetic(sX, sY) {
-    let kineticData = this._kineticData;
-
-    ws.dragStop();
-    this._owner.ungrab(this);
-    this._dragData.reset();
-    kineticData.reset();
-
-    // Make sure that sidebars don't stay partially open
-    // XXX this should live somewhere else
-    let [leftVis,] = ws.getWidgetVisibility("tabs-container", false);
-    let [rightVis,] = ws.getWidgetVisibility("browser-controls", false);
-    if (leftVis != 0 && leftVis != 1) {
-      let w = document.getElementById("tabs-container").getBoundingClientRect().width;
-      if (leftVis >= 0.6666)
-        ws.panBy(-w, 0, true);
-      else
-        ws.panBy(leftVis * w, 0, true);
-    }
-    else if (rightVis != 0 && rightVis != 1) {
-      let w = document.getElementById("browser-controls").getBoundingClientRect().width;
-      if (rightVis >= 0.6666)
-        ws.panBy(w, 0, true);
-      else
-        ws.panBy(-rightVis * w, 0, true);
-    }
-  }
+    if (dragData.dragging)
+      this._dragMove(sX, sY);
+  },
 };
 
 /**
  * Mouse click handlers
  */
 
 function ContentClickingModule(owner) {
   this._owner = owner;
@@ -730,51 +762,51 @@ ContentClickingModule.prototype = {
     switch (aEvent.type) {
       // UI panning events
       case "mousedown":
         this._events.push({event: aEvent, time: Date.now()});
 
         // we're waiting for a click
         if (this._clickTimeout != -1) {
           // if we just got another mousedown, don't send anything until we get another mousedown
-          clearTimeout(this._clickTimeout);
+          window.clearTimeout(this._clickTimeout);
           this.clickTimeout = -1;
         }
         break;
       case "mouseup":
         // keep an eye out for mouseups that didn't start with a mousedown
         if (!(this._events.length % 2)) {
           this._reset();
           break;
         }
 
         this._events.push({event: aEvent, time: Date.now()});
 
         if (this._clickTimeout == -1) {
-          this._clickTimeout = setTimeout(function(self) { self._sendSingleClick(); }, 400, this);
+          this._clickTimeout = window.setTimeout(function(self) { self._sendSingleClick(); }, 400, this);
         }
         else {
-          clearTimeout(this._clickTimeout);
+          window.clearTimeout(this._clickTimeout);
           this.clickTimeout = -1;
           this._sendDoubleClick();
         }
         break;
     }
   },
 
   /* If someone else grabs events ahead of us, cancel any pending
    * timeouts we may have.
    */
   cancelPending: function cancelPending() {
     this._reset();
   },
 
   _reset: function _reset() {
     if (this._clickTimeout != -1)
-      clearTimeout(this._clickTimeout);
+      window.clearTimeout(this._clickTimeout);
     this._clickTimeout = -1;
 
     this._events = [];
   },
 
   _sendSingleClick: function _sendSingleClick() {
     this._owner.grab(this);
     this._dispatchContentMouseEvent(this._events[0].event);
--- a/mobile/chrome/content/WidgetStack.js
+++ b/mobile/chrome/content/WidgetStack.js
@@ -129,22 +129,22 @@ function wsRect(x, y, w, h) {
 
 wsRect.prototype = {
   _l: 0, _t: 0, _b: 0, _r: 0,
 
   get x() { return this._l; },
   get y() { return this._t; },
   get width() { return this._r - this._l; },
   get height() { return this._b - this._t; },
-  set x(v) { 
+  set x(v) {
     let diff = this._l - v;
     this._l = v;
-    this._r -= diff; 
+    this._r -= diff;
   },
-  set y(v) { 
+  set y(v) {
     let diff = this._t - v;
     this._t = v;
     this._b -= diff;
   },
   set width(v) { this._r = this._l + v; },
   set height(v) { this._b = this._t + v; },
 
   get left() { return this._l; },
@@ -235,23 +235,23 @@ wsRect.prototype = {
               other._b <= this._b);
   },
 
   intersect: function(r2) {
     let xmost1 = this._r;
     let xmost2 = r2._r;
 
     let x = Math.max(this._l, r2._l);
-    
+
     let temp = Math.min(xmost1, xmost2);
     if (temp <= x)
       return null;
 
     let width = temp - x;
-    
+
     let ymost1 = this._b;
     let ymost2 = r2._b;
     let y = Math.max(this._t, r2._t);
 
     temp = Math.min(ymost1, ymost2);
     if (temp <= y)
       return null;
 
@@ -264,17 +264,17 @@ wsRect.prototype = {
     let xok = (other._l > this._l && other._l < this._r) ||
       (other._r > this._l && other._r < this._r) ||
       (other._l <= this._l && other._r >= this._r);
     let yok = (other._t > this._t && other._t < this._b) ||
       (other._b > this._t && other._b < this._b) ||
       (other._t <= this._t && other._b >= this._b);
     return xok && yok;
   },
-  
+
   round: function(scale) {
     this._l = Math.floor(this._l * scale) / scale;
     this._t = Math.floor(this._t * scale) / scale;
     this._r = Math.ceil(this._r * scale) / scale;
     this._b = Math.ceil(this._b * scale) / scale;
   }
 };
 
@@ -347,17 +347,17 @@ WidgetStack.prototype = {
   _viewportUpdateTimeout: -1,
 
   _viewportUpdateHandler: null,
   _panHandler: null,
 
   _dragState: null,
 
   _skipViewportUpdates: 0,
-  
+
   //
   // init:
   //   el: the <stack> element whose children are to be managed
   //
   init: function (el, ew, eh) {
     this._el = el;
     this._widgetState = {};
     this._barriers = [];
@@ -411,32 +411,34 @@ WidgetStack.prototype = {
   // panBy: pan the entire set of widgets by the given x and y amounts.
   // This does the same thing as if the user dragged by the given amount.
   // If this is called with an outstanding drag, weirdness might happen,
   // but it also might work, so not disabling that.
   //
   // if ignoreBarriers is true, then barriers are ignored for the pan.
   panBy: function panBy(dx, dy, ignoreBarriers) {
     if (dx == 0 && dy ==0)
-      return;
+      return false;
 
     let needsDragWrap = !this._dragging;
 
     if (needsDragWrap)
       this.dragStart(0, 0);
 
-    this._panBy(dx, dy, ignoreBarriers);
+    let panned = this._panBy(dx, dy, ignoreBarriers);
 
     if (needsDragWrap)
       this.dragStop();
+
+    return panned;
   },
 
   // panTo: pan the entire set of widgets so that the given x,y coordinates
   // are in the upper left of the stack.
-  panTo: function (x, y) {
+  panTo: function panTo(x, y) {
     this.panBy(x - this._viewingRect.x, y - this._viewingRect.y, true);
   },
 
   // freeze: set a widget as frozen.  A frozen widget won't be moved
   // in the stack -- its x,y position will still be tracked in the
   // state, but the left/top attributes won't be overwritten.  Call unfreeze
   // to move the widget back to where the ws thinks it should be.
   freeze: function (wid) {
@@ -619,17 +621,17 @@ WidgetStack.prototype = {
   //
   // The given functin object is called whenever elements pan; it provides
   // the new area of the pannable bounds that's visible in the stack.
   setPanHandler: function (uh) {
     this._panHandler = uh;
   },
 
   // dragStart: start a drag, with the current coordinates being clientX,clientY
-  dragStart: function (clientX, clientY) {
+  dragStart: function dragStart(clientX, clientY) {
     log("(dragStart)", clientX, clientY);
 
     if (this._dragState) {
       reportError("dragStart with drag already in progress? what?");
       this._dragState = null;
     }
 
     this._dragState = { };
@@ -653,47 +655,47 @@ WidgetStack.prototype = {
       // deltas correctly to update the viewport
       this._viewport.dragStartRect = this._viewport.rect.clone();
     }
 
     this._dragState.dragging = true;
   },
 
   // dragStop: stop any drag in progress
-  dragStop: function () {
+  dragStop: function dragStop() {
     log("(dragStop)");
 
     if (!this._dragging)
       return;
 
     if (this._viewportUpdateTimeout != -1)
       clearTimeout(this._viewportUpdateTimeout);
 
     this._viewportUpdate();
 
     this._dragState = null;
   },
 
   // dragMove: process a mouse move to clientX,clientY for an ongoing drag
-  dragMove: function dragStop(clientX, clientY) {
+  dragMove: function dragMove(clientX, clientY) {
     if (!this._dragging)
-      return;
-
-    log("(dragMove)", clientX, clientY);
+      return false;
 
     this._dragCoordsFromClient(clientX, clientY);
 
-    this._dragUpdate();
+    let panned = this._dragUpdate();
 
     if (this._viewportUpdateInterval != -1) {
       if (this._viewportUpdateTimeout != -1)
         clearTimeout(this._viewportUpdateTimeout);
       let self = this;
       this._viewportUpdateTimeout = setTimeout(function () { self._viewportUpdate(); }, this._viewportUpdateInterval);
     }
+
+    return panned;
   },
 
   // updateSize: tell the WidgetStack to update its size, because it
   // was either resized or some other event took place.
   updateSize: function updateSize(width, height) {
     if (width == undefined || height == undefined) {
       let rect = this._el.getBoundingClientRect();
       width = rect.width;
@@ -721,24 +723,24 @@ WidgetStack.prototype = {
     this.endUpdateBatch();
   },
 
   beginUpdateBatch: function startUpdate() {
     if (!this._skipViewportUpdates)
       this._startViewportBoundsString = this._viewportBounds.toString();
     this._skipViewportUpdates++;
   },
-  
+
   endUpdateBatch: function endUpdate() {
     if (!this._skipViewportUpdates)
       throw new Error("Unbalanced call to endUpdateBatch");
     this._skipViewportUpdates--;
     if (this._skipViewportUpdates)
       return
-    
+
     let boundsSizeChanged =
       this._startViewportBoundsString != this._viewportBounds.toString();
     this._callViewportUpdateHandler(boundsSizeChanged);
   },
 
   //
   // Internal code
   //
@@ -795,20 +797,20 @@ WidgetStack.prototype = {
       panY = pb.bottom - vr.bottom;
     else if(vr.top < pb.top)
       panY = pb.top - vr.top;
 
     this.panBy(panX, panY, true);
   },
 
   _getState: function (wid) {
-    let w = this._widgetState[wid]; 
+    let w = this._widgetState[wid];
     if (!w)
       throw "Unknown widget id '" + wid + "'; widget not in stack";
-    return w; 
+    return w;
   },
 
   get _dragging() {
     return this._dragState && this._dragState.dragging;
   },
 
   _viewportUpdate: function _viewportUpdate() {
     if (!this._viewport)
@@ -1024,17 +1026,17 @@ WidgetStack.prototype = {
     // direction of the pan, so we fiddle with the signs here (as you
     // pan to the upper left, more of the bottom right becomes visible,
     // so the viewing rect moves to the bottom right of the virtual surface).
     [dx, dy] = this._rectTranslateConstrain(dx, dy, vr, this.pannableBounds);
 
     // If the net result is that we don't have any room to move, then
     // just return.
     if (dx == 0 && dy == 0)
-      return;
+      return false;
 
     // the viewingRect moves opposite of the actual pan direction, see above
     vr.x += dx;
     vr.y += dy;
 
     // Go through each widget and move it by dx,dy.  Frozen widgets
     // will be ignored in commitState.
     // The widget rects are in real stack space though, so we need to subtract
@@ -1049,26 +1051,28 @@ WidgetStack.prototype = {
     }
 
     /* Do not call panhandler during pans within a transaction.
      * Those pans always end-up covering up the checkerboard and
      * do not require sliding out the location bar
      */
     if (!this._skipViewportUpdates && this._panHandler)
       this._panHandler.apply(window, [vr.clone(), dx, dy]);
+
+    return true;
   },
 
-  _dragUpdate: function () {
+  _dragUpdate: function _dragUpdate() {
     let dx = this._dragState.outerLastUpdateDX - this._dragState.outerDX;
     let dy = this._dragState.outerLastUpdateDY - this._dragState.outerDY;
 
     this._dragState.outerLastUpdateDX = this._dragState.outerDX;
     this._dragState.outerLastUpdateDY = this._dragState.outerDY;
 
-    this.panBy(dx, dy);
+    return this.panBy(dx, dy);
   },
 
   //
   // widget addition/removal
   //
   _addNewWidget: function (w) {
     let wid = w.getAttribute("id");
     if (!wid) {
@@ -1234,23 +1238,23 @@ WidgetStack.prototype = {
   _commitState: function (state) {
     // if the widget is frozen, don't actually update its left/top;
     // presumably the caller is managing those directly for now.
     if (state.frozen)
       return;
     let w = state.widget;
     let l = state.rect.x + state.offsetLeft;
     let t = state.rect.y + state.offsetTop;
-    
+
     //cache left/top to avoid calling setAttribute unnessesarily
     if (state._left != l) {
       state._left = l;
       w.setAttribute("left", l);
-    } 
-    
+    }
+
     if (state._top != t) {
       state._top = t;
       w.setAttribute("top", t);
     }
   },
 
   // constrain translate of rect by dx dy to bounds; return dx dy that can
   // be used to bring rect up to the edge of bounds if we'd go over.