Bug 501566 - It takes way too long to highlight a selected link [r=mfinkle]
authorBenjamin Stover <bstover@mozilla.com>
Fri, 11 Dec 2009 11:41:28 -0800
changeset 65909 52c10b08328c6bf2dab99e32adca47083c9ab257
parent 65908 73f16b4596a52045e4ef7405703c30769b45cb2f
child 65910 9318d62cb05d9355b58df5e56381f0bcedcb6df9
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle
bugs501566
Bug 501566 - It takes way too long to highlight a selected link [r=mfinkle]
mobile/chrome/content/InputHandler.js
mobile/chrome/content/browser.js
mobile/chrome/content/browser.xul
mobile/chrome/content/content.css
--- a/mobile/chrome/content/InputHandler.js
+++ b/mobile/chrome/content/InputHandler.js
@@ -528,25 +528,23 @@ MouseModule.prototype = {
    * (which, do note, might just be the beginning of a kinetic drag that will
    * linger long after we are gone), and recording the mousedown for later
    * redispatching.
    *
    * We ungrab() in here.
    */
   _onMouseUp: function _onMouseUp(evInfo) {
     let dragData = this._dragData;
+    let oldIsPan = dragData.isPan();
     if (dragData.dragging) {
       dragData.setDragPosition(evInfo.event.screenX, evInfo.event.screenY);
       let [sX, sY] = dragData.panPosition();
       this._doDragStop(sX, sY, !dragData.isPan());
     }
 
-    if (this._clicker)
-      this._clicker.mouseUp(evInfo.event.clientX, evInfo.event.clientY);
-
     if (this._targetIsContent(evInfo.event)) {
       // User possibly clicked on something in content
       this._recordEvent(evInfo);
       let commitToClicker = this._clicker && dragData.isClick();
       if (commitToClicker)
         // commit this click to the doubleclick timewait buffer
         this._commitAnotherClick();
       else
@@ -559,33 +557,47 @@ MouseModule.prototype = {
       // User was panning around, do not allow chrome click
       // XXX Instead of having suppressNextClick, we could grab until click is seen
       // and THEN ungrab so that owner does not need to know anything about clicking.
       let generatesClick = evInfo.event.detail;
       if (generatesClick)
         this._owner.suppressNextClick();
     }
 
+    let clicker = this._clicker;
+    if (clicker) {
+      // Let clicker know when mousemove begins a pan
+      if (!oldIsPan && dragData.isPan())
+        clicker.panBegin();
+      clicker.mouseUp(evInfo.event.clientX, evInfo.event.clientY);
+    }
+
     this._owner.ungrab(this);
   },
 
   /**
    * If we're in a drag, do what we have to do to drag on.
    */
   _onMouseMove: function _onMouseMove(evInfo) {
     let dragData = this._dragData;
 
     if (dragData.dragging) {
+      let oldIsPan = dragData.isPan();
       dragData.setDragPosition(evInfo.event.screenX, evInfo.event.screenY);
       evInfo.event.stopPropagation();
       evInfo.event.preventDefault();
       if (dragData.isPan()) {
         // Only pan when mouse event isn't part of a click. Prevent jittering on tap.
         let [sX, sY] = dragData.panPosition();
         this._doDragMove(sX, sY);
+
+        // Let clicker know when mousemove begins a pan
+        let clicker = this._clicker;
+        if (!oldIsPan && clicker)
+          clicker.panBegin();
       }
     }
   },
 
   /**
    * Check if the event concern the browser content
    */
   _targetIsContent: function _targetIsContent(aEvent) {
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -1139,16 +1139,43 @@ var Browser = {
     let zoomLevel = bv.getZoomForPage();
     if (!bv.isDefaultZoom()) {
       let [elementX, elementY] = this.transformClientToBrowser(cX, cY);
       let zoomRect = this._getZoomRectForPoint(elementX, elementY, zoomLevel);
       this.setVisibleRect(zoomRect);
     }
   },
 
+  getContentClientRects: function getContentClientRects(contentElem) {
+    // XXX don't copy getBoundingContentRect
+    let browser = Browser._browserView.getBrowser();
+
+    if (!browser)
+      return null;
+
+    let offset = BrowserView.Util.getContentScrollOffset(browser);
+    let nativeRects = contentElem.getClientRects();
+
+    // step out of iframes and frames, offsetting scroll values
+    let rect;
+    for (let frame = contentElem.ownerDocument.defaultView; frame != browser.contentWindow; frame = frame.parent) {
+      // adjust client coordinates' origin to be top left of iframe viewport
+      rect = frame.frameElement.getBoundingClientRect();
+      offset.add(rect.left, rect.top);
+    }
+
+    let result = [];
+    let r;
+    for (let i = nativeRects.length - 1; i >= 0; i--) {
+      r = nativeRects[i];
+      result.push(new Rect(r.left + offset.x, r.top + offset.y, r.width, r.height));
+    }
+    return result;
+  },
+
   getBoundingContentRect: function getBoundingContentRect(contentElem) {
     let document = contentElem.ownerDocument;
     while(document.defaultView.frameElement)
       document = document.defaultView.frameElement.ownerDocument;
 
     let tab = Browser.getTabForDocument(document);
     if (!tab || !tab.browser)
       return null;
@@ -1697,38 +1724,107 @@ const BrowserSearch = {
       button.engine = engine;
     }
   }
 }
 
 /** Watches for mouse events in chrome and sends them to content. */
 function ContentCustomClicker(browserView) {
   this._browserView = browserView;
+  this._overlay = document.getElementById("content-overlay");
+  this._width = 0;
+  this._height = 0;
 }
 
 ContentCustomClicker.prototype = {
     /** Dispatch a mouse event with chrome client coordinates. */
     _dispatchMouseEvent: function _dispatchMouseEvent(name, cX, cY) {
       let browser = this._browserView.getBrowser();
       if (browser) {
         let [x, y] = Browser.transformClientToBrowser(cX, cY);
         let cwu = BrowserView.Util.getBrowserDOMWindowUtils(browser);
         let scrollX = {}, scrollY = {};
         cwu.getScrollXY(false, scrollX, scrollY);
         cwu.sendMouseEvent(name, x - scrollX.value, y - scrollY.value, 0, 1, 0, true);
       }
     },
 
+    /** Returns a node if selecting this node causes a focus. */
+    _getFocusable: function(node) {
+      if (node && node.mozMatchesSelector("*:link,*:visited,*:link *,*:visited *,button,input,option,select,textarea"))
+        return node;
+      return null;
+    },
+ 
+    /** Stop highlighting current element. */
+    _hideCanvas: function _hideCanvas() {
+      let overlay = this._overlay;
+      overlay.style.display = "none";
+      overlay.getContext("2d").clearRect(0, 0, this._width, this._height);
+    },
+
+    /** Make sure canvas is at least width x height. */
+    _ensureSize: function _ensureSize(width, height) {
+      if (this._width <= width) {
+        this._width = width;
+        this._overlay.width = width;
+      }
+      if (this._height <= height) {
+        this._height = height;
+        this._overlay.height = height;
+      }
+    },
+
     mouseDown: function mouseDown(cX, cY) {
+      // This code is sensitive to performance. Please profile changes you make to
+      // keep this running fast.
+
+      let bv = this._browserView;
+      let overlay = this._overlay;
+      let ctx = overlay.getContext("2d");
+      let [elementX, elementY] = Browser.transformClientToBrowser(cX, cY);
+      let element = this._getFocusable(Browser.elementFromPoint(elementX, elementY));
+      if (!element)
+        return;
+
+      let rects = Browser.getContentClientRects(element);
+      let union = rects.reduce(function(a, b) {
+        return a.expandToContain(b);
+      }, new Rect(0, 0, 0, 0)).map(bv.browserToViewport);
+
+      let vis = Browser.getVisibleRect();
+      let canvasArea = vis.intersect(union);
+      this._ensureSize(canvasArea.width, canvasArea.height);
+
+      ctx.save();
+      ctx.translate(-canvasArea.left, -canvasArea.top);
+      bv.browserToViewportCanvasContext(ctx);
+
+      overlay.style.left = canvasArea.left + "px";
+      overlay.style.top = canvasArea.top + "px";
+      ctx.fillStyle = "rgba(0, 145, 255, .5)";
+      let rect;
+      for (let i = rects.length - 1; i >= 0; i--) {
+        rect = rects[i];
+        ctx.fillRect(rect.left, rect.top, rect.width, rect.height);
+      }
+      ctx.restore();
+      overlay.style.display = "block";
     },
 
     mouseUp: function mouseUp(cX, cY) {
     },
 
+    panBegin: function panBegin() {
+      this._hideCanvas();
+    },
+
     singleClick: function singleClick(cX, cY, modifiers) {
+      this._hideCanvas();
+
       let [elementX, elementY] = Browser.transformClientToBrowser(cX, cY);
       let element = Browser.elementFromPoint(elementX, elementY);
       if (modifiers == 0) {
         if (element instanceof HTMLOptionElement)
           element = element.parentNode;
 
         if (FormHelper.canShowUIFor(element)) {
           FormHelper.open(element);
@@ -1741,16 +1837,18 @@ ContentCustomClicker.prototype = {
       else if (modifiers == Ci.nsIDOMNSEvent.CONTROL_MASK) {
         let uri = Util.getHrefForElement(element);
         if (uri)
           Browser.addTab(uri, false);
       }
     },
 
     doubleClick: function doubleClick(cX1, cY1, cX2, cY2) {
+      this._hideCanvas();
+
       const kDoubleClickRadius = 32;
       
       let maxRadius = kDoubleClickRadius * Browser._browserView.getZoomLevel();
       let isClickInRadius = (Math.abs(cX1 - cX2) < maxRadius && Math.abs(cY1 - cY2) < maxRadius);
       if (isClickInRadius && !Browser.zoomToPoint(cX1, cY1))
         Browser.zoomFromPoint(cX1, cY1);
     },
 
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -237,19 +237,21 @@
 
           <notificationbox id="notifications" class="window-width"/>
 
           <!-- Content viewport -->
           <vbox class="window-width window-height">
             <stack id="tile-stack" class="window-width" flex="1">
               <scrollbox id="content-scrollbox" style="overflow: hidden;" class="window-width" flex="1">
                 <!-- Content viewport -->
-                <html:div id="tile-container" style="overflow: hidden;"/>
+                <html:div id="tile-container" style="overflow: hidden;">
+                  <html:canvas id="content-overlay" style="display: none; position: absolute; z-index: 1000;">
+                  </html:canvas>
+                </html:div>
               </scrollbox>
-
               <html:canvas id="view-buffer" style="display: none;" moz-opaque="true">
               </html:canvas>
             </stack>
             <box id="form-helper-spacer" hidden="true"/>
           </vbox>
 
         </vbox>
       </scrollbox>
--- a/mobile/chrome/content/content.css
+++ b/mobile/chrome/content/content.css
@@ -33,29 +33,30 @@
  * 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 ***** */
 
 /* make clicking on links stand out a bit (bug 532206) */
 html *:not(embed):focus, *:focus > font {
-  color: #000000 !important;
-  background-color: #ffffa0 !important;
+  outline: 2px solid #8db8d8 !important;
+  /*
+   XXX How do I preserve mac focusring without blowing focus color on other platforms?
+   outline-color: -moz-mac-focusring !important;
+   */
 }
 
 html *|*:link:focus, *|*:visited:focus {
-  outline: 1px solid -moz-mac-focusring !important;
-  -moz-outline-radius: 3px;
-  outline-offset: 1px;
+  outline-offset: -2px;
 }
 
 html button::-moz-focus-inner, input[type="reset"]::-moz-focus-inner, input[type="button"]::-moz-focus-inner, input[type="submit"]::-moz-focus-inner {
   padding: 1px 2px 1px 2px;
-  border: 1px solid transparent !important;
+  border: 0px !important;
 } 
 
 html button:focus::-moz-focus-inner, input[type="reset"]:focus::-moz-focus-inner, input[type="button"]:focus::-moz-focus-inner, input[type="submit"]:focus::-moz-focus-inner {
   border-color: ButtonText !important;
 }
 
 /* Style the scrollbars */
 scrollbar {