Bug 582865: add Geometry.jsm to toolkit, brought over from Mobile
authorMichael Yoshitaka Erlewine <mitcho@mitcho.com>
Mon, 09 Aug 2010 14:18:22 -0400
changeset 50283 ef29ba8395859ac014ece58d8fe6c0980c9baa9c
parent 50282 2cf27b575827aa5140864ad55e9fb090378716b5
child 50284 9a7a4256f84260f5c9c231f433f75029b1b6f705
push idunknown
push userunknown
push dateunknown
bugs582865
milestone2.0b4pre
Bug 582865: add Geometry.jsm to toolkit, brought over from Mobile
toolkit/content/Geometry.jsm
toolkit/content/Makefile.in
toolkit/content/tests/browser/Makefile.in
toolkit/content/tests/browser/browser_Geometry.js
new file mode 100644
--- /dev/null
+++ b/toolkit/content/Geometry.jsm
@@ -0,0 +1,364 @@
+/*
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Roy Frostig <rfrostig@mozilla.com>
+ *   Ben Combee <bcombee@mozilla.com>
+ *   Matt Brubeck <mbrubeck@mozilla.com>
+ *   Michael Yoshitaka Erlewine <mitcho@mitcho.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ["Point","Rect"];
+
+/**
+ * Simple Point class.
+ *
+ * Any method that takes an x and y may also take a point.
+ */
+function Point(x, y) {
+  this.set(x, y);
+}
+
+Point.prototype = {
+  clone: function clone() {
+    return new Point(this.x, this.y);
+  },
+
+  set: function set(x, y) {
+    this.x = x;
+    this.y = y;
+    return this;
+  },
+  
+  equals: function equals(x, y) {
+    return this.x == x && this.y == y;
+  },
+
+  toString: function toString() {
+    return "(" + this.x + "," + this.y + ")";
+  },
+
+  map: function map(f) {
+    this.x = f.call(this, this.x);
+    this.y = f.call(this, this.y);
+    return this;
+  },
+
+  add: function add(x, y) {
+    this.x += x;
+    this.y += y;
+    return this;
+  },
+
+  subtract: function subtract(x, y) {
+    this.x -= x;
+    this.y -= y;
+    return this;
+  },
+
+  scale: function scale(s) {
+    this.x *= s;
+    this.y *= s;
+    return this;
+  },
+
+  isZero: function() {
+    return this.x == 0 && this.y == 0;
+  }
+};
+
+(function() {
+  function takePointOrArgs(f) {
+    return function(arg1, arg2) {
+      if (arg2 === undefined)
+        return f.call(this, arg1.x, arg1.y);
+      else
+        return f.call(this, arg1, arg2);
+    };
+  }
+
+  for each (let f in ['add', 'subtract', 'equals', 'set'])
+    Point.prototype[f] = takePointOrArgs(Point.prototype[f]);
+})();
+
+
+/**
+ * Rect is a simple data structure for representation of a rectangle supporting
+ * many basic geometric operations.
+ *
+ * NOTE: Since its operations are closed, rectangles may be empty and will report
+ * non-positive widths and heights in that case.
+ */
+
+function Rect(x, y, w, h) {
+  this.left = x;
+  this.top = y;
+  this.right = x + w;
+  this.bottom = y + h;
+};
+
+Rect.fromRect = function fromRect(r) {
+  return new Rect(r.left, r.top, r.right - r.left, r.bottom - r.top);
+};
+
+Rect.prototype = {
+  get x() { return this.left; },
+  get y() { return this.top; },
+  get width() { return this.right - this.left; },
+  get height() { return this.bottom - this.top; },
+  set x(v) {
+    let diff = this.left - v;
+    this.left = v;
+    this.right -= diff;
+  },
+  set y(v) {
+    let diff = this.top - v;
+    this.top = v;
+    this.bottom -= diff;
+  },
+  set width(v) { this.right = this.left + v; },
+  set height(v) { this.bottom = this.top + v; },
+
+  isEmpty: function isEmpty() {
+    return this.left >= this.right || this.top >= this.bottom;
+  },
+
+  setRect: function(x, y, w, h) {
+    this.left = x;
+    this.top = y;
+    this.right = x+w;
+    this.bottom = y+h;
+
+    return this;
+  },
+
+  setBounds: function(l, t, r, b) {
+    this.top = t;
+    this.left = l;
+    this.bottom = b;
+    this.right = r;
+
+    return this;
+  },
+
+  equals: function equals(other) {
+    return other != null &&
+            (this.isEmpty() && other.isEmpty() ||
+            this.top == other.top &&
+            this.left == other.left &&
+            this.bottom == other.bottom &&
+            this.right == other.right);
+  },
+
+  clone: function clone() {
+    return new Rect(this.left, this.top, this.right - this.left, this.bottom - this.top);
+  },
+
+  center: function center() {
+    if (this.isEmpty())
+      throw "Empty rectangles do not have centers";
+    return new Point(this.left + (this.right - this.left) / 2,
+                          this.top + (this.bottom - this.top) / 2);
+  },
+
+  copyFrom: function(other) {
+    this.top = other.top;
+    this.left = other.left;
+    this.bottom = other.bottom;
+    this.right = other.right;
+
+    return this;
+  },
+
+  translate: function(x, y) {
+    this.left += x;
+    this.right += x;
+    this.top += y;
+    this.bottom += y;
+
+    return this;
+  },
+
+  toString: function() {
+    return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]";
+  },
+
+  /** return a new rect that is the union of that one and this one */
+  union: function(other) {
+    return this.clone().expandToContain(other);
+  },
+
+  contains: function(other) {
+    if (other.isEmpty()) return true;
+    if (this.isEmpty()) return false;
+
+    return (other.left >= this.left &&
+            other.right <= this.right &&
+            other.top >= this.top &&
+            other.bottom <= this.bottom);
+  },
+
+  intersect: function(other) {
+    return this.clone().restrictTo(other);
+  },
+
+  intersects: function(other) {
+    if (this.isEmpty() || other.isEmpty())
+      return false;
+
+    let x1 = Math.max(this.left, other.left);
+    let x2 = Math.min(this.right, other.right);
+    let y1 = Math.max(this.top, other.top);
+    let y2 = Math.min(this.bottom, other.bottom);
+    return x1 < x2 && y1 < y2;
+  },
+
+  /** Restrict area of this rectangle to the intersection of both rectangles. */
+  restrictTo: function restrictTo(other) {
+    if (this.isEmpty() || other.isEmpty())
+      return this.setRect(0, 0, 0, 0);
+
+    let x1 = Math.max(this.left, other.left);
+    let x2 = Math.min(this.right, other.right);
+    let y1 = Math.max(this.top, other.top);
+    let y2 = Math.min(this.bottom, other.bottom);
+    // If width or height is 0, the intersection was empty.
+    return this.setRect(x1, y1, Math.max(0, x2 - x1), Math.max(0, y2 - y1));
+  },
+
+  /** Expand this rectangle to the union of both rectangles. */
+  expandToContain: function expandToContain(other) {
+    if (this.isEmpty()) return this.copyFrom(other);
+    if (other.isEmpty()) return this;
+
+    let l = Math.min(this.left, other.left);
+    let r = Math.max(this.right, other.right);
+    let t = Math.min(this.top, other.top);
+    let b = Math.max(this.bottom, other.bottom);
+    return this.setRect(l, t, r-l, b-t);
+  },
+
+  /**
+   * Expands to the smallest rectangle that contains original rectangle and is bounded
+   * by lines with integer coefficients.
+   */
+  expandToIntegers: function round() {
+    this.left = Math.floor(this.left);
+    this.top = Math.floor(this.top);
+    this.right = Math.ceil(this.right);
+    this.bottom = Math.ceil(this.bottom);
+    return this;
+  },
+
+  scale: function scale(xscl, yscl) {
+    this.left *= xscl;
+    this.right *= xscl;
+    this.top *= yscl;
+    this.bottom *= yscl;
+    return this;
+  },
+
+  map: function map(f) {
+    this.left = f.call(this, this.left);
+    this.top = f.call(this, this.top);
+    this.right = f.call(this, this.right);
+    this.bottom = f.call(this, this.bottom);
+    return this;
+  },
+
+  /** Ensure this rectangle is inside the other, if possible. Preserves w, h. */
+  translateInside: function translateInside(other) {
+    let offsetX = (this.left < other.left ? other.left - this.left :
+        (this.right > other.right ? other.right - this.right : 0));
+    let offsetY = (this.top < other.top ? other.top - this.top :
+        (this.bottom > other.bottom ? other.bottom - this.bottom : 0));
+    return this.translate(offsetX, offsetY);
+  },
+
+  /** Subtract other area from this. Returns array of rects whose union is this-other. */
+  subtract: function subtract(other) {
+    let r = new Rect(0, 0, 0, 0);
+    let result = [];
+    other = other.intersect(this);
+    if (other.isEmpty())
+      return [this.clone()];
+
+    // left strip
+    r.setBounds(this.left, this.top, other.left, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    // inside strip
+    r.setBounds(other.left, this.top, other.right, other.top);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    r.setBounds(other.left, other.bottom, other.right, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+    // right strip
+    r.setBounds(other.right, this.top, this.right, this.bottom);
+    if (!r.isEmpty())
+      result.push(r.clone());
+
+    return result;
+  },
+
+  /**
+   * Blends two rectangles together.
+   * @param rect Rectangle to blend this one with
+   * @param scalar Ratio from 0 (returns a clone of this rect) to 1 (clone of rect).
+   * @return New blended rectangle.
+   */
+  blend: function blend(rect, scalar) {
+    return new Rect(
+      this.left   + (rect.left   - this.left  ) * scalar,
+      this.top    + (rect.top    - this.top   ) * scalar,
+      this.width  + (rect.width  - this.width ) * scalar,
+      this.height + (rect.height - this.height) * scalar);
+  },
+
+  /**
+   * Grows or shrinks the rectangle while keeping the center point.
+   * Accepts single multipler, or separate for both axes.
+   */
+  inflate: function inflate(xscl, yscl) {
+    let xAdj = (this.width * xscl - this.width) / 2;
+    let s = (arguments.length > 1) ? yscl : xscl;
+    let yAdj = (this.height * s - this.height) / 2;
+    this.left -= xAdj;
+    this.right += xAdj;
+    this.top -= yAdj;
+    this.bottom += yAdj;
+    return this;
+  }
+};
--- a/toolkit/content/Makefile.in
+++ b/toolkit/content/Makefile.in
@@ -80,16 +80,17 @@ endif
 
 ifdef ENABLE_TESTS
 DIRS += tests
 endif
 
 EXTRA_JS_MODULES = \
   InlineSpellChecker.jsm \
   PopupNotifications.jsm \
+  Geometry.jsm \
   $(NULL)
 
 EXTRA_PP_JS_MODULES = \
   debug.js \
   LightweightThemeConsumer.jsm \
   Services.jsm \
   WindowDraggingUtils.jsm \
   $(NULL)
--- a/toolkit/content/tests/browser/Makefile.in
+++ b/toolkit/content/tests/browser/Makefile.in
@@ -50,12 +50,13 @@ DIRS = \
 
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
   $(warning browser_keyevents_during_autoscrolling.js disabled due to frequent timeouts (bug 567950)) \
   browser_bug295977_autoscroll_overflow.js \
   browser_save_resend_postdata.js \
   browser_Services.js \
+  browser_Geometry.js \
   $(NULL)
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/browser/browser_Geometry.js
@@ -0,0 +1,140 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Benjamin Stover <bstover@mozilla.com> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/Geometry.jsm");
+
+function test() {
+  ok(Rect, "Rect class exists");
+  for (var fname in tests) {
+    tests[fname]();
+  }
+}
+
+let tests = {
+  testGetDimensions: function() {
+    let r = new Rect(5, 10, 100, 50);
+    ok(r.left == 5, "rect has correct left value");
+    ok(r.top == 10, "rect has correct top value");
+    ok(r.right == 105, "rect has correct right value");
+    ok(r.bottom == 60, "rect has correct bottom value");
+    ok(r.width == 100, "rect has correct width value");
+    ok(r.height == 50, "rect has correct height value");
+    ok(r.x == 5, "rect has correct x value");
+    ok(r.y == 10, "rect has correct y value");
+  },
+
+  testIsEmpty: function() {
+    let r = new Rect(0, 0, 0, 10);
+    ok(r.isEmpty(), "rect with nonpositive width is empty");
+    let r = new Rect(0, 0, 10, 0);
+    ok(r.isEmpty(), "rect with nonpositive height is empty");
+    let r = new Rect(0, 0, 10, 10);
+    ok(!r.isEmpty(), "rect with positive dimensions is not empty");
+  },
+
+  testRestrictTo: function() {
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(50, 50, 100, 100);
+    r1.restrictTo(r2);
+    ok(r1.equals(new Rect(50, 50, 60, 60)), "intersection is non-empty");
+
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(120, 120, 100, 100);
+    r1.restrictTo(r2);
+    ok(r1.isEmpty(), "intersection is empty");
+
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(0, 0, 0, 0);
+    r1.restrictTo(r2);
+    ok(r1.isEmpty(), "intersection of rect and empty is empty");
+
+    let r1 = new Rect(0, 0, 0, 0);
+    let r2 = new Rect(0, 0, 0, 0);
+    r1.restrictTo(r2);
+    ok(r1.isEmpty(), "intersection of empty and empty is empty");
+  },
+
+  testExpandToContain: function() {
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(50, 50, 100, 100);
+    r1.expandToContain(r2);
+    ok(r1.equals(new Rect(10, 10, 140, 140)), "correct expandToContain on intersecting rectangles");
+
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(120, 120, 100, 100);
+    r1.expandToContain(r2);
+    ok(r1.equals(new Rect(10, 10, 210, 210)), "correct expandToContain on non-intersecting rectangles");
+
+    let r1 = new Rect(10, 10, 100, 100);
+    let r2 = new Rect(0, 0, 0, 0);
+    r1.expandToContain(r2);
+    ok(r1.equals(new Rect(10, 10, 100, 100)), "expandToContain of rect and empty is rect");
+
+    let r1 = new Rect(10, 10, 0, 0);
+    let r2 = new Rect(0, 0, 0, 0);
+    r1.expandToContain(r2);
+    ok(r1.isEmpty(), "expandToContain of empty and empty is empty");
+  },
+
+  testSubtract: function testSubtract() {
+    function equals(rects1, rects2) {
+      return rects1.length == rects2.length && rects1.every(function(r, i) {
+        return r.equals(rects2[i]);
+      });
+    }
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(500, 500, 100, 100);
+    ok(equals(r1.subtract(r2), [r1]), "subtract area outside of region yields same region");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(-10, -10, 50, 120);
+    ok(equals(r1.subtract(r2), [new Rect(40, 0, 60, 100)]), "subtracting vertical bar from edge leaves one rect");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(-10, -10, 120, 50);
+    ok(equals(r1.subtract(r2), [new Rect(0, 40, 100, 60)]), "subtracting horizontal bar from edge leaves one rect");
+
+    let r1 = new Rect(0, 0, 100, 100);
+    let r2 = new Rect(40, 40, 20, 20);
+    ok(equals(r1.subtract(r2), [
+      new Rect(0, 0, 40, 100),
+      new Rect(40, 0, 20, 40),
+      new Rect(40, 60, 20, 40),
+      new Rect(60, 0, 40, 100)]),
+      "subtracting rect in middle leaves union of rects");
+  },
+};