PageRenderTime 76ms CodeModel.GetById 11ms app.highlight 54ms RepoModel.GetById 1ms app.codeStats 1ms

/toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js

http://github.com/zpao/v8monkey
JavaScript | 1465 lines | 913 code | 281 blank | 271 comment | 258 complexity | f4a3ba8e0be58bf4b9725f1c1f0f8b38 MD5 | raw file
   1/* -*- Mode: js2; tab-width: 40; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- */
   2/*
   3 * ***** BEGIN LICENSE BLOCK *****
   4 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
   5 *
   6 * The contents of this file are subject to the Mozilla Public License Version
   7 * 1.1 (the "License"); you may not use this file except in compliance with
   8 * the License. You may obtain a copy of the License at
   9 * http://www.mozilla.org/MPL/
  10 *
  11 * Software distributed under the License is distributed on an "AS IS" basis,
  12 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  13 * for the specific language governing rights and limitations under the
  14 * License.
  15 *
  16 * The Original Code is Mozilla Mobile Browser.
  17 *
  18 * The Initial Developer of the Original Code is
  19 * Mozilla Corporation.
  20 * Portions created by the Initial Developer are Copyright (C) 2008
  21 * the Initial Developer. All Rights Reserved.
  22 *
  23 * Contributor(s):
  24 *   Vladimir Vukicevic <vladimir@pobox.com>
  25 *
  26 * Alternatively, the contents of this file may be used under the terms of
  27 * either the GNU General Public License Version 2 or later (the "GPL"), or
  28 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  29 * in which case the provisions of the GPL or the LGPL are applicable instead
  30 * of those above. If you wish to allow use of your version of this file only
  31 * under the terms of either the GPL or the LGPL, and not to allow others to
  32 * use your version of this file under the terms of the MPL, indicate your
  33 * decision by deleting the provisions above and replace them with the notice
  34 * and other provisions required by the GPL or the LGPL. If you do not delete
  35 * the provisions above, a recipient may use your version of this file under
  36 * the terms of any one of the MPL, the GPL or the LGPL.
  37 *
  38 * ***** END LICENSE BLOCK ***** */
  39
  40var gWsDoLog = false;
  41var gWsLogDiv = null;
  42
  43function logbase() {
  44  if (!gWsDoLog)
  45    return;
  46
  47  if (gWsLogDiv == null && "console" in window) {
  48    console.log.apply(console, arguments);
  49  } else {
  50    var s = "";
  51    for (var i = 0; i < arguments.length; i++) {
  52      s += arguments[i] + " ";
  53    }
  54    s += "\n";
  55    if (gWsLogDiv) {
  56      gWsLogDiv.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "br"));
  57      gWsLogDiv.appendChild(document.createTextNode(s));
  58    }
  59
  60    dump(s);
  61  }
  62}
  63
  64function dumpJSStack(stopAtNamedFunction) {
  65  let caller = Components.stack.caller;
  66  dump("\tStack: " + caller.name);
  67  while ((caller = caller.caller)) {
  68    dump(" <- " + caller.name);
  69    if (stopAtNamedFunction && caller.name != "anonymous")
  70      break;
  71  }
  72  dump("\n");
  73}
  74
  75function log() {
  76  return;
  77  logbase.apply(window, arguments);
  78}
  79
  80function log2() {
  81  return;
  82  logbase.apply(window, arguments);
  83}
  84
  85let reportError = log;
  86
  87/*
  88 * wsBorder class
  89 *
  90 * Simple container for top,left,bottom,right "border" values
  91 */
  92function wsBorder(t, l, b, r) {
  93  this.setBorder(t, l, b, r);
  94}
  95
  96wsBorder.prototype = {
  97
  98  setBorder: function (t, l, b, r) {
  99    this.top = t;
 100    this.left = l;
 101    this.bottom = b;
 102    this.right = r;
 103  },
 104
 105  toString: function () {
 106    return "[l:" + this.left + ",t:" + this.top + ",r:" + this.right + ",b:" + this.bottom + "]";
 107  }
 108};
 109
 110/*
 111 * wsRect class
 112 *
 113 * Rectangle class, with both x/y/w/h and t/l/b/r accessors.
 114 */
 115function wsRect(x, y, w, h) {
 116  this.left = x;
 117  this.top = y;
 118  this.right = x+w;
 119  this.bottom = y+h;
 120}
 121
 122wsRect.prototype = {
 123
 124  get x() { return this.left; },
 125  get y() { return this.top; },
 126  get width() { return this.right - this.left; },
 127  get height() { return this.bottom - this.top; },
 128  set x(v) {
 129    let diff = this.left - v;
 130    this.left = v;
 131    this.right -= diff;
 132  },
 133  set y(v) {
 134    let diff = this.top - v;
 135    this.top = v;
 136    this.bottom -= diff;
 137  },
 138  set width(v) { this.right = this.left + v; },
 139  set height(v) { this.bottom = this.top + v; },
 140
 141  setRect: function(x, y, w, h) {
 142    this.left = x;
 143    this.top = y;
 144    this.right = x+w;
 145    this.bottom = y+h;
 146
 147    return this;
 148  },
 149
 150  setBounds: function(t, l, b, r) {
 151    this.top = t;
 152    this.left = l;
 153    this.bottom = b;
 154    this.right = r;
 155
 156    return this;
 157  },
 158
 159  equals: function equals(r) {
 160    return (r != null       &&
 161            this.top == r.top &&
 162            this.left == r.left &&
 163            this.bottom == r.bottom &&
 164            this.right == r.right);
 165  },
 166
 167  clone: function clone() {
 168    return new wsRect(this.left, this.top, this.right - this.left, this.bottom - this.top);
 169  },
 170
 171  center: function center() {
 172    return [this.left + (this.right - this.left) / 2,
 173            this.top + (this.bottom - this.top) / 2];
 174  },
 175
 176  centerRounded: function centerRounded() {
 177    return this.center().map(Math.round);
 178  },
 179
 180  copyFrom: function(r) {
 181    this.top = r.top;
 182    this.left = r.left;
 183    this.bottom = r.bottom;
 184    this.right = r.right;
 185
 186    return this;
 187  },
 188
 189  copyFromTLBR: function(r) {
 190    this.left = r.left;
 191    this.top = r.top;
 192    this.right = r.right;
 193    this.bottom = r.bottom;
 194
 195    return this;
 196  },
 197
 198  translate: function(x, y) {
 199    this.left += x;
 200    this.right += x;
 201    this.top += y;
 202    this.bottom += y;
 203
 204    return this;
 205  },
 206
 207  // return a new wsRect that is the union of that one and this one
 208  union: function(rect) {
 209    let l = Math.min(this.left, rect.left);
 210    let r = Math.max(this.right, rect.right);
 211    let t = Math.min(this.top, rect.top);
 212    let b = Math.max(this.bottom, rect.bottom);
 213
 214    return new wsRect(l, t, r-l, b-t);
 215  },
 216
 217  toString: function() {
 218    return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]";
 219  },
 220
 221  expandBy: function(b) {
 222    this.left += b.left;
 223    this.right += b.right;
 224    this.top += b.top;
 225    this.bottom += b.bottom;
 226    return this;
 227  },
 228
 229  contains: function(other) {
 230    return !!(other.left >= this.left &&
 231              other.right <= this.right &&
 232              other.top >= this.top &&
 233              other.bottom <= this.bottom);
 234  },
 235
 236  intersect: function(r2) {
 237    let xmost1 = this.right;
 238    let xmost2 = r2.right;
 239
 240    let x = Math.max(this.left, r2.left);
 241
 242    let temp = Math.min(xmost1, xmost2);
 243    if (temp <= x)
 244      return null;
 245
 246    let width = temp - x;
 247
 248    let ymost1 = this.bottom;
 249    let ymost2 = r2.bottom;
 250    let y = Math.max(this.top, r2.top);
 251
 252    temp = Math.min(ymost1, ymost2);
 253    if (temp <= y)
 254      return null;
 255
 256    let height = temp - y;
 257
 258    return new wsRect(x, y, width, height);
 259  },
 260
 261  intersects: function(other) {
 262    let xok = (other.left > this.left && other.left < this.right) ||
 263      (other.right > this.left && other.right < this.right) ||
 264      (other.left <= this.left && other.right >= this.right);
 265    let yok = (other.top > this.top && other.top < this.bottom) ||
 266      (other.bottom > this.top && other.bottom < this.bottom) ||
 267      (other.top <= this.top && other.bottom >= this.bottom);
 268    return xok && yok;
 269  },
 270
 271  /**
 272   * Similar to (and most code stolen from) intersect().  A restriction
 273   * is an intersection, but this modifies the receiving object instead
 274   * of returning a new rect.
 275   */
 276  restrictTo: function restrictTo(r2) {
 277    let xmost1 = this.right;
 278    let xmost2 = r2.right;
 279
 280    let x = Math.max(this.left, r2.left);
 281
 282    let temp = Math.min(xmost1, xmost2);
 283    if (temp <= x)
 284      throw "Intersection is empty but rects cannot be empty";
 285
 286    let width = temp - x;
 287
 288    let ymost1 = this.bottom;
 289    let ymost2 = r2.bottom;
 290    let y = Math.max(this.top, r2.top);
 291
 292    temp = Math.min(ymost1, ymost2);
 293    if (temp <= y)
 294      throw "Intersection is empty but rects cannot be empty";
 295
 296    let height = temp - y;
 297
 298    return this.setRect(x, y, width, height);
 299  },
 300
 301  /**
 302   * Similar to (and most code stolen from) union().  An extension is a
 303   * union (in our sense of the term, not the common set-theoretic sense),
 304   * but this modifies the receiving object instead of returning a new rect.
 305   * Effectively, this rectangle is expanded minimally to contain all of the
 306   * other rect.  "Expanded minimally" means that the rect may shrink if
 307   * given a strict subset rect as the argument.
 308   */
 309  expandToContain: function extendTo(rect) {
 310    let l = Math.min(this.left, rect.left);
 311    let r = Math.max(this.right, rect.right);
 312    let t = Math.min(this.top, rect.top);
 313    let b = Math.max(this.bottom, rect.bottom);
 314
 315    return this.setRect(l, t, r-l, b-t);
 316  },
 317
 318  round: function round(scale) {
 319    if (!scale) scale = 1;
 320
 321    this.left = Math.floor(this.left * scale) / scale;
 322    this.top = Math.floor(this.top * scale) / scale;
 323    this.right = Math.ceil(this.right * scale) / scale;
 324    this.bottom = Math.ceil(this.bottom * scale) / scale;
 325
 326    return this;
 327  },
 328
 329  scale: function scale(xscl, yscl) {
 330    this.left *= xscl;
 331    this.right *= xscl;
 332    this.top *= yscl;
 333    this.bottom *= yscl;
 334
 335    return this;
 336  }
 337};
 338
 339/*
 340 * The "Widget Stack"
 341 *
 342 * Manages a <xul:stack>'s children, allowing them to be dragged around
 343 * the stack, subject to specified constraints.  Optionally supports
 344 * one widget designated as the viewport, which can be panned over a virtual
 345 * area without needing to draw that area entirely.  The viewport widget
 346 * is designated by a 'viewport' attribute on the child element.
 347 *
 348 * Widgets are subject to various constraints, specified in xul via the
 349 * 'constraint' attribute.  Current constraints are:
 350 *   ignore-x: When panning, ignore any changes to the widget's x position
 351 *   ignore-y: When panning, ignore any changes to the widget's y position
 352 *   vp-relative: This widget's position should be claculated relative to
 353 *     the viewport widget.  It will always keep the same offset from that
 354 *     widget as initially laid out, regardless of changes to the viewport
 355 *     bounds.
 356 *   frozen: This widget is in a fixed position and should never pan.
 357 */
 358function WidgetStack(el, ew, eh) {
 359  this.init(el, ew, eh);
 360}
 361
 362WidgetStack.prototype = {
 363  // the <stack> element
 364  _el: null,
 365
 366  // object indexed by widget id, with state struct for each object (see _addNewWidget)
 367  _widgetState: null,
 368
 369  // any barriers
 370  _barriers: null,
 371
 372  // If a viewport widget is present, this will point to its state object;
 373  // otherwise null.
 374  _viewport: null,
 375
 376  // a wsRect; the inner bounds of the viewport content
 377  _viewportBounds: null,
 378  // a wsBorder; the overflow area to the side of the bounds where our
 379  // viewport-relative widgets go
 380  _viewportOverflow: null,
 381
 382  // a wsRect; the viewportBounds expanded by the viewportOverflow
 383  _pannableBounds: null,
 384  get pannableBounds() {
 385    if (!this._pannableBounds) {
 386      this._pannableBounds = this._viewportBounds.clone()
 387                                 .expandBy(this._viewportOverflow);
 388    }
 389    return this._pannableBounds.clone();
 390  },
 391
 392  // a wsRect; the currently visible part of pannableBounds.
 393  _viewingRect: null,
 394
 395  // the amount of current global offset applied to all widgets (whether
 396  // static or not).  Set via offsetAll().  Can be used to push things
 397  // out of the way for overlaying some other UI.
 398  globalOffsetX: 0,
 399  globalOffsetY: 0,
 400
 401  // if true (default), panning is constrained to the pannable bounds.
 402  _constrainToViewport: true,
 403
 404  _viewportUpdateInterval: -1,
 405  _viewportUpdateTimeout: -1,
 406
 407  _viewportUpdateHandler: null,
 408  _panHandler: null,
 409
 410  _dragState: null,
 411
 412  _skipViewportUpdates: 0,
 413  _forceViewportUpdate: false,
 414
 415  //
 416  // init:
 417  //   el: the <stack> element whose children are to be managed
 418  //
 419  init: function (el, ew, eh) {
 420    this._el = el;
 421    this._widgetState = {};
 422    this._barriers = [];
 423
 424    let rect = this._el.getBoundingClientRect();
 425    let width = rect.width;
 426    let height = rect.height;
 427
 428    if (ew != undefined && eh != undefined) {
 429      width = ew;
 430      height = eh;
 431    }
 432
 433    this._viewportOverflow = new wsBorder(0, 0, 0, 0);
 434
 435    this._viewingRect = new wsRect(0, 0, width, height);
 436
 437    // listen for DOMNodeInserted/DOMNodeRemoved/DOMAttrModified
 438    let children = this._el.childNodes;
 439    for (let i = 0; i < children.length; i++) {
 440      let c = this._el.childNodes[i];
 441      if (c.tagName == "spacer")
 442        this._addNewBarrierFromSpacer(c);
 443      else
 444        this._addNewWidget(c);
 445    }
 446
 447    // this also updates the viewportOverflow and pannableBounds
 448    this._updateWidgets();
 449
 450    if (this._viewport) {
 451      this._viewportBounds = new wsRect(0, 0, this._viewport.rect.width, this._viewport.rect.height);
 452    } else {
 453      this._viewportBounds = new wsRect(0, 0, 0, 0);
 454    }
 455  },
 456
 457  // moveWidgetBy: move the widget with the given id by x,y.  Should
 458  // not be used on vp-relative or otherwise frozen widgets (using it
 459  // on the x coordinate for x-ignore widgets and similarily for y is
 460  // ok, as long as the other coordinate remains 0.)
 461  moveWidgetBy: function (wid, x, y) {
 462    let state = this._getState(wid);
 463
 464    state.rect.x += x;
 465    state.rect.y += y;
 466
 467    this._commitState(state);
 468  },
 469
 470  // panBy: pan the entire set of widgets by the given x and y amounts.
 471  // This does the same thing as if the user dragged by the given amount.
 472  // If this is called with an outstanding drag, weirdness might happen,
 473  // but it also might work, so not disabling that.
 474  //
 475  // if ignoreBarriers is true, then barriers are ignored for the pan.
 476  panBy: function panBy(dx, dy, ignoreBarriers) {
 477    dx = Math.round(dx);
 478    dy = Math.round(dy);
 479
 480    if (dx == 0 && dy == 0)
 481      return false;
 482
 483    let needsDragWrap = !this._dragging;
 484
 485    if (needsDragWrap)
 486      this.dragStart(0, 0);
 487
 488    let panned = this._panBy(dx, dy, ignoreBarriers);
 489
 490    if (needsDragWrap)
 491      this.dragStop();
 492
 493    return panned;
 494  },
 495
 496  // panTo: pan the entire set of widgets so that the given x,y
 497  // coordinates are in the upper left of the stack.  If either is
 498  // null or undefined, only move the other axis
 499  panTo: function panTo(x, y) {
 500    if (x == undefined || x == null)
 501      x = this._viewingRect.x;
 502    if (y == undefined || y == null)
 503      y = this._viewingRect.y;
 504    this.panBy(x - this._viewingRect.x, y - this._viewingRect.y, true);
 505  },
 506
 507  // freeze: set a widget as frozen.  A frozen widget won't be moved
 508  // in the stack -- its x,y position will still be tracked in the
 509  // state, but the left/top attributes won't be overwritten.  Call unfreeze
 510  // to move the widget back to where the ws thinks it should be.
 511  freeze: function (wid) {
 512    let state = this._getState(wid);
 513
 514    state.frozen = true;
 515  },
 516
 517  unfreeze: function (wid) {
 518    let state = this._getState(wid);
 519    if (!state.frozen)
 520      return;
 521
 522    state.frozen = false;
 523    this._commitState(state);
 524  },
 525
 526  // moveFrozenTo: move a frozen widget with id wid to x, y in the stack.
 527  // can only be used on frozen widgets
 528  moveFrozenTo: function (wid, x, y) {
 529    let state = this._getState(wid);
 530    if (!state.frozen)
 531      throw "moveFrozenTo on non-frozen widget " + wid;
 532
 533    state.widget.setAttribute("left", x);
 534    state.widget.setAttribute("top", y);
 535  },
 536
 537  // moveUnfrozenTo: move an unfrozen, pannable widget with id wid to x, y in
 538  // the stack. should only be used on unfrozen widgets when a dynamic change
 539  // in position needs to be made. we basically remove, adjust and re-add
 540  // the widget
 541  moveUnfrozenTo: function (wid, x, y) {
 542    delete this._widgetState[wid];
 543    let widget = document.getElementById(wid);
 544    if (x) widget.setAttribute("left", x);
 545    if (y) widget.setAttribute("top", y);
 546    this._addNewWidget(widget);
 547    this._updateWidgets();
 548  },
 549
 550  // we're relying on viewportBounds and viewingRect having the same origin
 551  get viewportVisibleRect () {
 552    let rect = this._viewportBounds.intersect(this._viewingRect);
 553    if (!rect)
 554        rect = new wsRect(0, 0, 0, 0);
 555    return rect;
 556  },
 557
 558  isWidgetFrozen: function isWidgetFrozen(wid) {
 559    return this._getState(wid).frozen;
 560  },
 561
 562  // isWidgetVisible: return true if any portion of widget with id wid is
 563  // visible; otherwise return false.
 564  isWidgetVisible: function (wid) {
 565    let state = this._getState(wid);
 566    let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
 567
 568    return visibleStackRect.intersects(state.rect);
 569  },
 570
 571  // getWidgetVisibility: returns the percentage that the widget is visible
 572  getWidgetVisibility: function (wid) {
 573    let state = this._getState(wid);
 574    let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
 575
 576    let visibleRect = visibleStackRect.intersect(state.rect);
 577    if (visibleRect)
 578      return [visibleRect.width / state.rect.width, visibleRect.height / state.rect.height]
 579
 580    return [0, 0];
 581  },
 582
 583  // offsetAll: add an offset to all widgets
 584  offsetAll: function (x, y) {
 585    this.globalOffsetX += x;
 586    this.globalOffsetY += y;
 587
 588    for each (let state in this._widgetState) {
 589      state.rect.x += x;
 590      state.rect.y += y;
 591
 592      this._commitState(state);
 593    }
 594  },
 595
 596  // setViewportBounds
 597  //  nb: an object containing top, left, bottom, right properties
 598  //    OR
 599  //  width, height: integer values; origin assumed to be 0,0
 600  //    OR
 601  //  top, left, bottom, right: integer values
 602  //
 603  // Set the bounds of the viewport area; that is, set the size of the
 604  // actual content that the viewport widget will be providing a view
 605  // over.  For example, in the case of a 100x100 viewport showing a
 606  // view into a 100x500 webpage, the viewport bounds would be
 607  // { top: 0, left: 0, bottom: 500, right: 100 }.
 608  //
 609  // setViewportBounds will move all the viewport-relative widgets into
 610  // place based on the new viewport bounds.
 611  setViewportBounds: function setViewportBounds() {
 612    let oldBounds = this._viewportBounds.clone();
 613
 614    if (arguments.length == 1) {
 615      this._viewportBounds.copyFromTLBR(arguments[0]);
 616    } else if (arguments.length == 2) {
 617      this._viewportBounds.setRect(0, 0, arguments[0], arguments[1]);
 618    } else if (arguments.length == 4) {
 619      this._viewportBounds.setBounds(arguments[0],
 620      arguments[1],
 621      arguments[2],
 622      arguments[3]);
 623    } else {
 624      throw "Invalid number of arguments to setViewportBounds";
 625    }
 626
 627    let vp = this._viewport;
 628
 629    let dleft = this._viewportBounds.left - oldBounds.left;
 630    let dright = this._viewportBounds.right - oldBounds.right;
 631    let dtop = this._viewportBounds.top - oldBounds.top;
 632    let dbottom = this._viewportBounds.bottom - oldBounds.bottom;
 633
 634    //log2("setViewportBounds dltrb", dleft, dtop, dright, dbottom);
 635
 636    // move all vp-relative widgets to be the right offset from the bounds again
 637    for each (let state in this._widgetState) {
 638      if (state.vpRelative) {
 639        //log2("vpRelative widget", state.id, state.rect.x, dleft, dright);
 640        if (state.vpOffsetXBefore) {
 641          state.rect.x += dleft;
 642        } else {
 643          state.rect.x += dright;
 644        }
 645
 646        if (state.vpOffsetYBefore) {
 647          state.rect.y += dtop;
 648        } else {
 649          state.rect.y += dbottom;
 650        }
 651
 652        //log2("vpRelative widget", state.id, state.rect.x, dleft, dright);
 653        this._commitState(state);
 654      }
 655    }
 656
 657    for (let bid in this._barriers) {
 658      let barrier = this._barriers[bid];
 659
 660      //log2("setViewportBounds: looking at barrier", bid, barrier.vpRelative, barrier.type);
 661
 662      if (barrier.vpRelative) {
 663        if (barrier.type == "vertical") {
 664          let q = "v barrier moving from " + barrier.x + " to ";
 665          if (barrier.vpOffsetXBefore) {
 666            barrier.x += dleft;
 667          } else {
 668            barrier.x += dright;
 669          }
 670          //log2(q += barrier.x);
 671        } else if (barrier.type == "horizontal") {
 672          let q = "h barrier moving from " + barrier.y + " to ";
 673          if (barrier.vpOffsetYBefore) {
 674            barrier.y += dtop;
 675          } else {
 676            barrier.y += dbottom;
 677          }
 678          //log2(q += barrier.y);
 679        }
 680      }
 681    }
 682
 683    // clear the pannable bounds cache to make sure it gets rebuilt
 684    this._pannableBounds = null;
 685
 686    // now let's make sure that the viewing rect and inner bounds are still valid
 687    this._adjustViewingRect();
 688
 689    this._viewportUpdate(0, 0, true);
 690  },
 691
 692  // setViewportHandler
 693  //  uh: A function object
 694  //
 695  // The given function object is called at the end of every drag and viewport
 696  // bounds change, passing in the new rect that's to be displayed in the
 697  // viewport.
 698  //
 699  setViewportHandler: function (uh) {
 700    this._viewportUpdateHandler = uh;
 701  },
 702
 703  // setPanHandler
 704  // uh: A function object
 705  //
 706  // The given functin object is called whenever elements pan; it provides
 707  // the new area of the pannable bounds that's visible in the stack.
 708  setPanHandler: function (uh) {
 709    this._panHandler = uh;
 710  },
 711
 712  // dragStart: start a drag, with the current coordinates being clientX,clientY
 713  dragStart: function dragStart(clientX, clientY) {
 714    log("(dragStart)", clientX, clientY);
 715
 716    if (this._dragState) {
 717      reportError("dragStart with drag already in progress? what?");
 718      this._dragState = null;
 719    }
 720
 721    this._dragState = { };
 722
 723    let t = Date.now();
 724
 725    this._dragState.barrierState = [];
 726
 727    this._dragState.startTime = t;
 728    // outer x, that is outer from the viewport coordinates.  In stack-relative coords.
 729    this._dragState.outerStartX = clientX;
 730    this._dragState.outerStartY = clientY;
 731
 732    this._dragCoordsFromClient(clientX, clientY, t);
 733
 734    this._dragState.outerLastUpdateDX = 0;
 735    this._dragState.outerLastUpdateDY = 0;
 736
 737    if (this._viewport) {
 738      // create a copy of these so that we can compute
 739      // deltas correctly to update the viewport
 740      this._viewport.dragStartRect = this._viewport.rect.clone();
 741    }
 742
 743    this._dragState.dragging = true;
 744  },
 745
 746  _viewportDragUpdate: function viewportDragUpdate() {
 747    let vws = this._viewport;
 748    this._viewportUpdate((vws.dragStartRect.x - vws.rect.x),
 749                         (vws.dragStartRect.y - vws.rect.y));
 750  },
 751
 752  // dragStop: stop any drag in progress
 753  dragStop: function dragStop() {
 754    log("(dragStop)");
 755
 756    if (!this._dragging)
 757      return;
 758
 759    if (this._viewportUpdateTimeout != -1)
 760      clearTimeout(this._viewportUpdateTimeout);
 761
 762    this._viewportDragUpdate();
 763
 764    this._dragState = null;
 765  },
 766
 767  // dragMove: process a mouse move to clientX,clientY for an ongoing drag
 768  dragMove: function dragMove(clientX, clientY) {
 769    if (!this._dragging)
 770      return false;
 771
 772    this._dragCoordsFromClient(clientX, clientY);
 773
 774    let panned = this._dragUpdate();
 775
 776    if (this._viewportUpdateInterval != -1) {
 777      if (this._viewportUpdateTimeout != -1)
 778        clearTimeout(this._viewportUpdateTimeout);
 779      let self = this;
 780      this._viewportUpdateTimeout = setTimeout(function () { self._viewportDragUpdate(); }, this._viewportUpdateInterval);
 781    }
 782
 783    return panned;
 784  },
 785
 786  // dragBy: process a mouse move by dx,dy for an ongoing drag
 787  dragBy: function dragBy(dx, dy) {
 788    return this.dragMove(this._dragState.outerCurX + dx, this._dragState.outerCurY + dy);
 789  },
 790
 791  // updateSize: tell the WidgetStack to update its size, because it
 792  // was either resized or some other event took place.
 793  updateSize: function updateSize(width, height) {
 794    if (width == undefined || height == undefined) {
 795      let rect = this._el.getBoundingClientRect();
 796      width = rect.width;
 797      height = rect.height;
 798    }
 799
 800    // update widget rects and viewportOverflow, since the resize might have
 801    // caused them to change (widgets first, since the viewportOverflow depends
 802    // on them).
 803
 804    // XXX these methods aren't working correctly yet, but they aren't strictly
 805    // necessary in Fennec's default config
 806    //for each (let s in this._widgetState)
 807    //  this._updateWidgetRect(s);
 808    //this._updateViewportOverflow();
 809
 810    this._viewingRect.width = width;
 811    this._viewingRect.height = height;
 812
 813    // Wrap this call in a batch to ensure that we always call the
 814    // viewportUpdateHandler, even if _adjustViewingRect doesn't trigger a pan.
 815    // If it does, the batch also ensures that we don't call the handler twice.
 816    this.beginUpdateBatch();
 817    this._adjustViewingRect();
 818    this.endUpdateBatch();
 819  },
 820
 821  beginUpdateBatch: function startUpdate() {
 822    if (!this._skipViewportUpdates) {
 823      this._startViewportBoundsString = this._viewportBounds.toString();
 824      this._forceViewportUpdate = false;
 825    }
 826    this._skipViewportUpdates++;
 827  },
 828
 829  endUpdateBatch: function endUpdate(aForceRedraw) {
 830    if (!this._skipViewportUpdates)
 831      throw new Error("Unbalanced call to endUpdateBatch");
 832
 833    this._forceViewportUpdate = this._forceViewportUpdate || aForceRedraw;
 834
 835    this._skipViewportUpdates--;
 836    if (this._skipViewportUpdates)
 837      return;
 838
 839    let boundsSizeChanged =
 840      this._startViewportBoundsString != this._viewportBounds.toString();
 841    this._callViewportUpdateHandler(boundsSizeChanged || this._forceViewportUpdate);
 842  },
 843
 844  //
 845  // Internal code
 846  //
 847
 848  _updateWidgetRect: function(state) {
 849    // don't need to support updating the viewport rect at the moment
 850    // (we'd need to duplicate the vptarget* code from _addNewWidget if we did)
 851    if (state == this._viewport)
 852      return;
 853
 854    let w = state.widget;
 855    let x = w.getAttribute("left") || 0;
 856    let y = w.getAttribute("top") || 0;
 857    let rect = w.getBoundingClientRect();
 858    state.rect = new wsRect(parseInt(x), parseInt(y),
 859                            rect.right - rect.left,
 860                            rect.bottom - rect.top);
 861    if (w.hasAttribute("widgetwidth") && w.hasAttribute("widgetheight")) {
 862      state.rect.width = parseInt(w.getAttribute("widgetwidth"));
 863      state.rect.height = parseInt(w.getAttribute("widgetheight"));
 864    }
 865  },
 866
 867  _dumpRects: function () {
 868    dump("WidgetStack:\n");
 869    dump("\tthis._viewportBounds: " + this._viewportBounds + "\n");
 870    dump("\tthis._viewingRect: " + this._viewingRect + "\n");
 871    dump("\tthis._viewport.viewportInnerBounds: " + this._viewport.viewportInnerBounds + "\n");
 872    dump("\tthis._viewport.rect: " + this._viewport.rect + "\n");
 873    dump("\tthis._viewportOverflow: " + this._viewportOverflow + "\n");
 874    dump("\tthis.pannableBounds: " + this.pannableBounds + "\n");
 875  },
 876
 877  // Ensures that _viewingRect is within _pannableBounds (call this when either
 878  // one is resized)
 879  _adjustViewingRect: function _adjustViewingRect() {
 880    let vr = this._viewingRect;
 881    let pb = this.pannableBounds;
 882
 883    if (pb.contains(vr))
 884      return; // nothing to do here
 885
 886    // don't bother adjusting _viewingRect if it can't fit into
 887    // _pannableBounds
 888    if (vr.height > pb.height || vr.width > pb.width)
 889      return;
 890
 891    let panX = 0, panY = 0;
 892    if (vr.right > pb.right)
 893      panX = pb.right - vr.right;
 894    else if (vr.left < pb.left)
 895      panX = pb.left - vr.left;
 896
 897    if (vr.bottom > pb.bottom)
 898      panY = pb.bottom - vr.bottom;
 899    else if(vr.top < pb.top)
 900      panY = pb.top - vr.top;
 901
 902    this.panBy(panX, panY, true);
 903  },
 904
 905  _getState: function (wid) {
 906    let w = this._widgetState[wid];
 907    if (!w)
 908      throw "Unknown widget id '" + wid + "'; widget not in stack";
 909    return w;
 910  },
 911
 912  get _dragging() {
 913    return this._dragState && this._dragState.dragging;
 914  },
 915
 916  _viewportUpdate: function _viewportUpdate(dX, dY, boundsChanged) {
 917    if (!this._viewport)
 918      return;
 919
 920    this._viewportUpdateTimeout = -1;
 921
 922    let vws = this._viewport;
 923    let vwib = vws.viewportInnerBounds;
 924    let vpb = this._viewportBounds;
 925
 926    // recover the amount the inner bounds moved by the amount the viewport
 927    // widget moved, but don't include offsets that we're making up from previous
 928    // drags that didn't affect viewportInnerBounds
 929    let [ignoreX, ignoreY] = this._offsets || [0, 0];
 930    let rx = dX - ignoreX;
 931    let ry = dY - ignoreY;
 932
 933    [dX, dY] = this._rectTranslateConstrain(rx, ry, vwib, vpb);
 934
 935    // record the offsets that correspond to the amount of the drag we're ignoring
 936    // to ensure the viewportInnerBounds remains within the viewportBounds
 937    this._offsets = [dX - rx, dY - ry];
 938
 939    // adjust the viewportInnerBounds, and snap the viewport back
 940    vwib.translate(dX, dY);
 941    vws.rect.translate(dX, dY);
 942    this._commitState(vws);
 943
 944    // update this so that we can call this function again during the same drag
 945    // and get the right values.
 946    vws.dragStartRect = vws.rect.clone();
 947
 948    this._callViewportUpdateHandler(boundsChanged);
 949  },
 950
 951  _callViewportUpdateHandler: function _callViewportUpdateHandler(boundsChanged) {
 952    if (!this._viewport || !this._viewportUpdateHandler || this._skipViewportUpdates)
 953      return;
 954
 955    let vwb = this._viewportBounds.clone();
 956
 957    let vwib = this._viewport.viewportInnerBounds.clone();
 958
 959    let vis = this.viewportVisibleRect;
 960
 961    vwib.left += this._viewport.offsetLeft;
 962    vwib.top += this._viewport.offsetTop;
 963    vwib.right += this._viewport.offsetRight;
 964    vwib.bottom += this._viewport.offsetBottom;
 965
 966    this._viewportUpdateHandler.apply(window, [vwb, vwib, vis, boundsChanged]);
 967  },
 968
 969  _dragCoordsFromClient: function (cx, cy, t) {
 970    this._dragState.curTime = t ? t : Date.now();
 971    this._dragState.outerCurX = cx;
 972    this._dragState.outerCurY = cy;
 973
 974    let dx = this._dragState.outerCurX - this._dragState.outerStartX;
 975    let dy = this._dragState.outerCurY - this._dragState.outerStartY;
 976    this._dragState.outerDX = dx;
 977    this._dragState.outerDY = dy;
 978  },
 979
 980  _panHandleBarriers: function (dx, dy) {
 981    // XXX unless the barriers are sorted by position, this will break
 982    // with multiple barriers that are near enough to eachother that a
 983    // drag could cross more than one.
 984
 985    let vr = this._viewingRect;
 986
 987    // XXX this just stops at the first horizontal and vertical barrier it finds
 988
 989    // barrier_[xy] is the barrier that was used to get to the final
 990    // barrier_d[xy] value.  if null, no barrier, and dx/dy shouldn't
 991    // be replaced with barrier_d[xy].
 992    let barrier_y = null, barrier_x = null;
 993    let barrier_dy = 0, barrier_dx = 0;
 994
 995    for (let i = 0; i < this._barriers.length; i++) {
 996      let b = this._barriers[i];
 997
 998      //log2("barrier", i, b.type, b.x, b.y);
 999
1000      if (dx != 0 && b.type == "vertical") {
1001        if (barrier_x != null) {
1002          delete this._dragState.barrierState[i];
1003          continue;
1004        }
1005
1006        let alreadyKnownDistance = this._dragState.barrierState[i] || 0;
1007
1008        //log2("alreadyKnownDistance", alreadyKnownDistance);
1009
1010        let dbx = 0;
1011
1012        //100 <= 100 && 100-(-5) > 100
1013
1014        if ((vr.left <= b.x && vr.left+dx > b.x) ||
1015            (vr.left >= b.x && vr.left+dx < b.x))
1016        {
1017          dbx = b.x - vr.left;
1018        } else if ((vr.right <= b.x && vr.right+dx > b.x) ||
1019                   (vr.right >= b.x && vr.right+dx < b.x))
1020        {
1021          dbx = b.x - vr.right;
1022        } else {
1023          delete this._dragState.barrierState[i];
1024          continue;
1025        }
1026
1027        let leftoverDistance = dbx - dx;
1028
1029        //log2("initial dbx", dbx, leftoverDistance);
1030
1031        let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size;
1032
1033        if (dist >= 0) {
1034          if (dx < 0)
1035            dbx -= dist;
1036          else
1037            dbx += dist;
1038          delete this._dragState.barrierState[i];
1039        } else {
1040          dbx = 0;
1041          this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance;
1042        }
1043
1044        //log2("final dbx", dbx, "state", this._dragState.barrierState[i]);
1045
1046        if (Math.abs(barrier_dx) <= Math.abs(dbx)) {
1047          barrier_x = b;
1048          barrier_dx = dbx;
1049
1050          //log2("new barrier_dx", barrier_dx);
1051        }
1052      }
1053
1054      if (dy != 0 && b.type == "horizontal") {
1055        if (barrier_y != null) {
1056          delete this._dragState.barrierState[i];
1057          continue;
1058        }
1059
1060        let alreadyKnownDistance = this._dragState.barrierState[i] || 0;
1061
1062        //log2("alreadyKnownDistance", alreadyKnownDistance);
1063
1064        let dby = 0;
1065
1066        //100 <= 100 && 100-(-5) > 100
1067
1068        if ((vr.top <= b.y && vr.top+dy > b.y) ||
1069            (vr.top >= b.y && vr.top+dy < b.y))
1070        {
1071          dby = b.y - vr.top;
1072        } else if ((vr.bottom <= b.y && vr.bottom+dy > b.y) ||
1073                   (vr.bottom >= b.y && vr.bottom+dy < b.y))
1074        {
1075          dby = b.y - vr.bottom;
1076        } else {
1077          delete this._dragState.barrierState[i];
1078          continue;
1079        }
1080
1081        let leftoverDistance = dby - dy;
1082
1083        //log2("initial dby", dby, leftoverDistance);
1084
1085        let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size;
1086
1087        if (dist >= 0) {
1088          if (dy < 0)
1089            dby -= dist;
1090          else
1091            dby += dist;
1092          delete this._dragState.barrierState[i];
1093        } else {
1094          dby = 0;
1095          this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance;
1096        }
1097
1098        //log2("final dby", dby, "state", this._dragState.barrierState[i]);
1099
1100        if (Math.abs(barrier_dy) <= Math.abs(dby)) {
1101          barrier_y = b;
1102          barrier_dy = dby;
1103
1104          //log2("new barrier_dy", barrier_dy);
1105        }
1106      }
1107    }
1108
1109    if (barrier_x) {
1110      //log2("did barrier_x", barrier_x, "barrier_dx", barrier_dx);
1111      dx = barrier_dx;
1112    }
1113
1114    if (barrier_y) {
1115      dy = barrier_dy;
1116    }
1117
1118    return [dx, dy];
1119  },
1120
1121  _panBy: function _panBy(dx, dy, ignoreBarriers) {
1122    let vr = this._viewingRect;
1123
1124    // check if any barriers would be crossed by this pan, and take them
1125    // into account.  do this first.
1126    if (!ignoreBarriers)
1127      [dx, dy] = this._panHandleBarriers(dx, dy);
1128
1129    // constrain the full drag of the viewingRect to the pannableBounds.
1130    // note that the viewingRect needs to move in the opposite
1131    // direction of the pan, so we fiddle with the signs here (as you
1132    // pan to the upper left, more of the bottom right becomes visible,
1133    // so the viewing rect moves to the bottom right of the virtual surface).
1134    [dx, dy] = this._rectTranslateConstrain(dx, dy, vr, this.pannableBounds);
1135
1136    // If the net result is that we don't have any room to move, then
1137    // just return.
1138    if (dx == 0 && dy == 0)
1139      return false;
1140
1141    // the viewingRect moves opposite of the actual pan direction, see above
1142    vr.x += dx;
1143    vr.y += dy;
1144
1145    // Go through each widget and move it by dx,dy.  Frozen widgets
1146    // will be ignored in commitState.
1147    // The widget rects are in real stack space though, so we need to subtract
1148    // our (now negated) dx, dy from their coordinates.
1149    for each (let state in this._widgetState) {
1150      if (!state.ignoreX)
1151        state.rect.x -= dx;
1152      if (!state.ignoreY)
1153        state.rect.y -= dy;
1154
1155      this._commitState(state);
1156    }
1157
1158    /* Do not call panhandler during pans within a transaction.
1159     * Those pans always end-up covering up the checkerboard and
1160     * do not require sliding out the location bar
1161     */
1162    if (!this._skipViewportUpdates && this._panHandler)
1163      this._panHandler.apply(window, [vr.clone(), dx, dy]);
1164
1165    return true;
1166  },
1167
1168  _dragUpdate: function _dragUpdate() {
1169    let dx = this._dragState.outerLastUpdateDX - this._dragState.outerDX;
1170    let dy = this._dragState.outerLastUpdateDY - this._dragState.outerDY;
1171
1172    this._dragState.outerLastUpdateDX = this._dragState.outerDX;
1173    this._dragState.outerLastUpdateDY = this._dragState.outerDY;
1174
1175    return this.panBy(dx, dy);
1176  },
1177
1178  //
1179  // widget addition/removal
1180  //
1181  _addNewWidget: function (w) {
1182    let wid = w.getAttribute("id");
1183    if (!wid) {
1184      reportError("WidgetStack: child widget without id!");
1185      return;
1186    }
1187
1188    if (w.getAttribute("hidden") == "true")
1189      return;
1190
1191    let state = {
1192      widget: w,
1193      id: wid,
1194
1195      viewport: false,
1196      ignoreX: false,
1197      ignoreY: false,
1198      sticky: false,
1199      frozen: false,
1200      vpRelative: false,
1201
1202      offsetLeft: 0,
1203      offsetTop: 0,
1204      offsetRight: 0,
1205      offsetBottom: 0
1206    };
1207
1208    this._updateWidgetRect(state);
1209
1210    if (w.hasAttribute("constraint")) {
1211      let cs = w.getAttribute("constraint").split(",");
1212      for each (let s in cs) {
1213        if (s == "ignore-x")
1214          state.ignoreX = true;
1215        else if (s == "ignore-y")
1216          state.ignoreY = true;
1217        else if (s == "sticky")
1218          state.sticky = true;
1219        else if (s == "frozen") {
1220          state.frozen = true;
1221        } else if (s == "vp-relative")
1222          state.vpRelative = true;
1223      }
1224    }
1225
1226    if (w.hasAttribute("viewport")) {
1227      if (this._viewport)
1228        reportError("WidgetStack: more than one viewport canvas in stack!");
1229
1230      this._viewport = state;
1231      state.viewport = true;
1232
1233      if (w.hasAttribute("vptargetx") && w.hasAttribute("vptargety") &&
1234          w.hasAttribute("vptargetw") && w.hasAttribute("vptargeth"))
1235      {
1236        let wx = parseInt(w.getAttribute("vptargetx"));
1237        let wy = parseInt(w.getAttribute("vptargety"));
1238        let ww = parseInt(w.getAttribute("vptargetw"));
1239        let wh = parseInt(w.getAttribute("vptargeth"));
1240
1241        state.offsetLeft = state.rect.left - wx;
1242        state.offsetTop = state.rect.top - wy;
1243        state.offsetRight = state.rect.right - (wx + ww);
1244        state.offsetBottom = state.rect.bottom - (wy + wh);
1245
1246        state.rect = new wsRect(wx, wy, ww, wh);
1247      }
1248
1249      // initialize inner bounds to top-left
1250      state.viewportInnerBounds = new wsRect(0, 0, state.rect.width, state.rect.height);
1251    }
1252
1253    this._widgetState[wid] = state;
1254
1255    log ("(New widget: " + wid + (state.viewport ? " [viewport]" : "") + " at: " + state.rect + ")");
1256  },
1257
1258  _removeWidget: function (w) {
1259    let wid = w.getAttribute("id");
1260    delete this._widgetState[wid];
1261    this._updateWidgets();
1262  },
1263
1264  // updateWidgets:
1265  //   Go through all the widgets and figure out their viewport-relative offsets.
1266  // If the widget goes to the left or above the viewport widget, then
1267  // vpOffsetXBefore or vpOffsetYBefore is set.
1268  // See setViewportBounds for use of vpOffset* state variables, and for how
1269  // the actual x and y coords of each widget are calculated based on their offsets
1270  // and the viewport bounds.
1271  _updateWidgets: function () {
1272    let vp = this._viewport;
1273
1274    let ofRect = this._viewingRect.clone();
1275
1276    for each (let state in this._widgetState) {
1277      if (vp && state.vpRelative) {
1278        // compute the vpOffset from 0,0 assuming that the viewport rect is 0,0
1279        if (state.rect.left >= vp.rect.right) {
1280          state.vpOffsetXBefore = false;
1281          state.vpOffsetX = state.rect.left - vp.rect.width;
1282        } else {
1283          state.vpOffsetXBefore = true;
1284          state.vpOffsetX = state.rect.left - vp.rect.left;
1285        }
1286
1287        if (state.rect.top >= vp.rect.bottom) {
1288          state.vpOffsetYBefore = false;
1289          state.vpOffsetY = state.rect.top - vp.rect.height;
1290        } else {
1291          state.vpOffsetYBefore = true;
1292          state.vpOffsetY = state.rect.top - vp.rect.top;
1293        }
1294
1295        log("widget", state.id, "offset", state.vpOffsetX, state.vpOffsetXBefore ? "b" : "a", state.vpOffsetY, state.vpOffsetYBefore ? "b" : "a", "rect", state.rect);
1296      }
1297    }
1298
1299    this._updateViewportOverflow();
1300  },
1301
1302  // updates the viewportOverflow/pannableBounds
1303  _updateViewportOverflow: function() {
1304    let vp = this._viewport;
1305    if (!vp)
1306      return;
1307
1308    let ofRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
1309
1310    for each (let state in this._widgetState) {
1311      if (vp && state.vpRelative) {
1312        ofRect.left = Math.min(ofRect.left, state.rect.left);
1313        ofRect.top = Math.min(ofRect.top, state.rect.top);
1314        ofRect.right = Math.max(ofRect.right, state.rect.right);
1315        ofRect.bottom = Math.max(ofRect.bottom, state.rect.bottom);
1316      }
1317    }
1318
1319    // prevent the viewportOverflow from having positive top/left or negative
1320    // bottom/right values, which would otherwise happen if there aren't widgets
1321    // beyond each of those edges
1322    this._viewportOverflow = new wsBorder(
1323      /*top*/ Math.round(Math.min(ofRect.top, 0)),
1324      /*left*/ Math.round(Math.min(ofRect.left, 0)),
1325      /*bottom*/ Math.round(Math.max(ofRect.bottom - vp.rect.height, 0)),
1326      /*right*/ Math.round(Math.max(ofRect.right - vp.rect.width, 0))
1327    );
1328
1329    // clear the _pannableBounds cache, since it depends on the
1330    // viewportOverflow
1331    this._pannableBounds = null;
1332  },
1333
1334  _widgetBounds: function () {
1335    let r = new wsRect(0,0,0,0);
1336
1337    for each (let state in this._widgetState)
1338      r = r.union(state.rect);
1339
1340    return r;
1341  },
1342
1343  _commitState: function (state) {
1344    // if the widget is frozen, don't actually update its left/top;
1345    // presumably the caller is managing those directly for now.
1346    if (state.frozen)
1347      return;
1348    let w = state.widget;
1349    let l = state.rect.x + state.offsetLeft;
1350    let t = state.rect.y + state.offsetTop;
1351
1352    //cache left/top to avoid calling setAttribute unnessesarily
1353    if (state._left != l) {
1354      state._left = l;
1355      w.setAttribute("left", l);
1356    }
1357
1358    if (state._top != t) {
1359      state._top = t;
1360      w.setAttribute("top", t);
1361    }
1362  },
1363
1364  // constrain translate of rect by dx dy to bounds; return dx dy that can
1365  // be used to bring rect up to the edge of bounds if we'd go over.
1366  _rectTranslateConstrain: function (dx, dy, rect, bounds) {
1367    let newX, newY;
1368
1369    // If the rect is larger than the bounds, allow it to increase its overlap
1370    let woverflow = rect.width > bounds.width;
1371    let hoverflow = rect.height > bounds.height;
1372    if (woverflow || hoverflow) {
1373      let intersection = rect.intersect(bounds);
1374      let newIntersection = rect.clone().translate(dx, dy).intersect(bounds);
1375      if (woverflow)
1376        newX = (newIntersection.width > intersection.width) ? rect.x + dx : rect.x;
1377      if (hoverflow)
1378        newY = (newIntersection.height > intersection.height) ? rect.y + dy : rect.y;
1379    }
1380
1381    // Common case, rect fits within the bounds
1382    // clamp new X to within [bounds.left, bounds.right - rect.width],
1383    //       new Y to within [bounds.top, bounds.bottom - rect.height]
1384    if (isNaN(newX))
1385      newX = Math.min(Math.max(bounds.left, rect.x + dx), bounds.right - rect.width);
1386    if (isNaN(newY))
1387      newY = Math.min(Math.max(bounds.top, rect.y + dy), bounds.bottom - rect.height);
1388
1389    return [newX - rect.x, newY - rect.y];
1390  },
1391
1392  // add a new barrier from a <spacer>
1393  _addNewBarrierFromSpacer: function (el) {
1394    let t = el.getAttribute("barriertype");
1395
1396    // XXX implement these at some point
1397    // t != "lr" && t != "rl" &&
1398    // t != "tb" && t != "bt" &&
1399
1400    if (t != "horizontal" &&
1401        t != "vertical")
1402    {
1403      throw "Invalid barrier type: " + t;
1404    }
1405
1406    let x, y;
1407
1408    let barrier = {};
1409    let vp = this._viewport;
1410
1411    barrier.type = t;
1412
1413    if (el.getAttribute("left"))
1414      barrier.x = parseInt(el.getAttribute("left"));
1415    else if (el.getAttribute("top"))
1416      barrier.y = parseInt(el.getAttribute("top"));
1417    else
1418      throw "Barrier without top or left attribute";
1419
1420    if (el.getAttribute("size"))
1421      barrier.size = parseInt(el.getAttribute("size"));
1422    else
1423      barrier.size = 10;
1424
1425    if (el.hasAttribute("constraint")) {
1426      let cs = el.getAttribute("constraint").split(",");
1427      for each (let s in cs) {
1428        if (s == "ignore-x")
1429          barrier.ignoreX = true;
1430        else if (s == "ignore-y")
1431          barrier.ignoreY = true;
1432        else if (s == "sticky")
1433          barrier.sticky = true;
1434        else if (s == "frozen") {
1435          barrier.frozen = true;
1436        } else if (s == "vp-relative")
1437          barrier.vpRelative = true;
1438      }
1439    }
1440
1441    if (barrier.vpRelative) {
1442      if (barrier.type == "vertical") {
1443        if (barrier.x >= vp.rect.right) {
1444          barrier.vpOffsetXBefore = false;
1445          barrier.vpOffsetX = barrier.x - vp.rect.right;
1446        } else {
1447          barrier.vpOffsetXBefore = true;
1448          barrier.vpOffsetX = barrier.x - vp.rect.left;
1449        }
1450      } else if (barrier.type == "horizontal") {
1451        if (barrier.y >= vp.rect.bottom) {
1452          barrier.vpOffsetYBefore = false;
1453          barrier.vpOffsetY = barrier.y - vp.rect.bottom;
1454        } else {
1455          barrier.vpOffsetYBefore = true;
1456          barrier.vpOffsetY = barrier.y - vp.rect.top;
1457        }
1458
1459        //log2("h barrier relative", barrier.vpOffsetYBefore, barrier.vpOffsetY);
1460      }
1461    }
1462
1463    this._barriers.push(barrier);
1464  }
1465};