Bug 1279717 - inverse the mask to use a white background on pages with bright text color. r?jaws draft
authorMike de Boer <mdeboer@mozilla.com>
Thu, 16 Jun 2016 16:53:49 +0100
changeset 379697 699e3e5f843670e0ff668f3ae4fd9cf40998e0b4
parent 379696 275f4d4399fe5004449a7d9cf6370f5234749c7a
child 523543 de91c7c4f7f4551d8f6328e720973663f12d2da1
push id21025
push usermdeboer@mozilla.com
push dateThu, 16 Jun 2016 16:06:08 +0000
reviewersjaws
bugs1279717
milestone50.0a1
Bug 1279717 - inverse the mask to use a white background on pages with bright text color. r?jaws MozReview-Commit-ID: 6LLMkS7vhEf
toolkit/modules/FinderHighlighter.jsm
toolkit/modules/RemoteFinder.jsm
toolkit/modules/tests/browser/browser_FinderHighlighter.js
--- a/toolkit/modules/FinderHighlighter.jsm
+++ b/toolkit/modules/FinderHighlighter.jsm
@@ -5,27 +5,31 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["FinderHighlighter"];
 
 const { interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm");
 
 const kHighlightIterationSizeMax = 100;
 const kModalHighlightRepaintFreqMs = 10;
 const kModalHighlightPref = "findbar.modalHighlight";
-const kFontPropsCSS = ["font-family", "font-kerning", "font-size", "font-size-adjust",
-  "font-stretch", "font-variant", "font-weight", "letter-spacing", "text-emphasis",
-  "text-orientation", "text-transform", "word-spacing"];
+const kFontPropsCSS = ["color", "font-family", "font-kerning", "font-size",
+  "font-size-adjust", "font-stretch", "font-variant", "font-weight", "letter-spacing",
+  "text-emphasis", "text-orientation", "text-transform", "word-spacing"];
 const kFontPropsCamelCase = kFontPropsCSS.map(prop => {
   let parts = prop.split("-");
   return parts.shift() + parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join("");
 });
+const kRGBRE = /^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*/i
 // This uuid is used to prefix HTML element IDs and classNames in order to make
 // them unique and hard to clash with IDs and classNames content authors come up
 // with, since the stylesheet for modal highlighting is inserted as an agent-sheet
 // in the active HTML document.
 const kModalIdPrefix = "cedee4d0-74c5-4f2d-ab43-4d37c0f9d463";
 const kModalOutlineId = kModalIdPrefix + "-findbar-modalHighlight-outline";
 const kModalStyle = `
 .findbar-modalHighlight-outline {
@@ -65,20 +69,28 @@ const kModalStyle = `
 .findbar-modalHighlight-outlineMask {
   background: #000;
   mix-blend-mode: multiply;
   opacity: .2;
   position: absolute;
   z-index: 1;
 }
 
+.findbar-modalHighlight-outlineMask[brighttext] {
+  background: #fff;
+}
+
 .findbar-modalHighlight-rect {
   background: #fff;
   border: 1px solid #666;
   position: absolute;
+}
+
+.findbar-modalHighlight-outlineMask[brighttext] > .findbar-modalHighlight-rect {
+  background: #000;
 }`;
 const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
  * FinderHighlighter class that is used by Finder.jsm to take care of the
  * 'Highlight All' feature, which can highlight all find occurrences in a page.
  *
  * @param {Finder} finder Finder.jsm instance
@@ -273,16 +285,17 @@ FinderHighlighter.prototype = {
       return;
 
     if (this._modalHighlightOutline)
       this._modalHighlightOutline.setAttributeForElement(kModalOutlineId, "hidden", "true");
 
     window = window || this.finder._getWindow();
     this._removeHighlightAllMask(window);
     this._removeModalHighlightListeners(window);
+    delete this._brightText;
   },
 
   /**
    * Called by the Finder after a find result comes in; update the position and
    * content of the outline to the newly found occurrence.
    * To make sure that the outline covers the found range completely, all the
    * CSS styles that influence the text are copied and applied to the outline.
    *
@@ -316,16 +329,22 @@ FinderHighlighter.prototype = {
     let textContent = this._getRangeContentArray(foundRange);
     if (!textContent.length) {
       this.hide(window);
       return;
     }
 
     let rect = foundRange.getBoundingClientRect();
     let fontStyle = this._getRangeFontStyle(foundRange);
+    if (typeof this._brightText == "undefined") {
+      this._brightText = this._isTextColorBright(fontStyle.color);
+    }
+
+    // Text color in the outline is determined by our stylesheet.
+    delete fontStyle.color;
 
     let anonNode = this.show(window);
 
     anonNode.setTextContentForElement(kModalOutlineId + "-text", textContent.join(" "));
     anonNode.setAttributeForElement(kModalOutlineId + "-text", "style",
       this._getHTMLFontStyle(fontStyle));
 
     if (typeof anonNode.getAttributeForElement(kModalOutlineId, "hidden") == "string")
@@ -493,16 +512,30 @@ FinderHighlighter.prototype = {
       if (idx == -1)
         continue
       style.push(`${kFontPropsCSS[idx]}: ${fontStyle[prop]};`);
     }
     return style.join(" ");
   },
 
   /**
+   * Checks whether a CSS RGB color value can be classified as being 'bright'.
+   *
+   * @param  {String} cssColor RGB color value in the default format rgb[a](r,g,b)
+   * @return {Boolean}
+   */
+  _isTextColorBright(cssColor) {
+    cssColor = cssColor.match(kRGBRE);
+    if (!cssColor || !cssColor.length)
+      return false;
+    cssColor.shift();
+    return new Color(...cssColor).relativeLuminance > 0.7;
+  },
+
+  /**
    * Add a range to the list of ranges to highlight on, or cut out of, the dimmed
    * background.
    *
    * @param {nsIDOMRange}  range  Range object that should be inspected
    * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed
    */
   _modalHighlight(range, controller, window) {
     if (!this._getRangeContentArray(range).length)
@@ -594,16 +627,18 @@ FinderHighlighter.prototype = {
     const kMaskId = kModalIdPrefix + "-findbar-modalHighlight-outlineMask";
     let maskNode = document.createElement("div");
 
     // Make sure the dimmed mask node takes the full width and height that's available.
     let {width, height} = this._getWindowDimensions(window);
     maskNode.setAttribute("id", kMaskId);
     maskNode.setAttribute("class", kMaskId);
     maskNode.setAttribute("style", `width: ${width}px; height: ${height}px;`);
+    if (this._brightText)
+      maskNode.setAttribute("brighttext", "true");
 
     // Create a DOM node for each rectangle representing the ranges we found.
     let maskContent = [];
     const kRectClassName = kModalIdPrefix + "-findbar-modalHighlight-rect";
     if (this._modalHighlightRectsMap) {
       for (let rects of this._modalHighlightRectsMap.values()) {
         for (let rect of rects) {
           maskContent.push(`<div class="${kRectClassName}" style="top: ${rect.y}px;
--- a/toolkit/modules/RemoteFinder.jsm
+++ b/toolkit/modules/RemoteFinder.jsm
@@ -305,13 +305,13 @@ RemoteFinderListener.prototype = {
         this._finder.keyPress(data);
         break;
 
       case "Finder:MatchesCount":
         this._finder.requestMatchesCount(data.searchString, data.matchLimit, data.linksOnly);
         break;
 
       case "Finder:ModalHighlightChange":
-        this._finder.ModalHighlightChange(data.useModalHighlight);
+        this._finder.onModalHighlightChange(data.useModalHighlight);
         break;
     }
   }
 };
--- a/toolkit/modules/tests/browser/browser_FinderHighlighter.js
+++ b/toolkit/modules/tests/browser/browser_FinderHighlighter.js
@@ -46,18 +46,19 @@ function promiseEnterStringIntoFindField
     let event = document.createEvent("KeyEvents");
     event.initKeyEvent("keypress", true, true, null, false, false,
                        false, false, 0, str.charCodeAt(i));
     findbar._findField.inputField.dispatchEvent(event);
   }
   return promise;
 }
 
-function promiseTestHighlighterOutput(browser, word, expectedResult) {
-  return ContentTask.spawn(browser, { word, expectedResult }, function* ({ word, expectedResult }) {
+function promiseTestHighlighterOutput(browser, word, expectedResult, extraTest = () => {}) {
+  return ContentTask.spawn(browser, { word, expectedResult, extraTest: extraTest.toSource() },
+    function* ({ word, expectedResult, extraTest }) {
     let document = content.document;
 
     return new Promise((resolve, reject) => {
       let stubbed = [document.insertAnonymousContent,
         document.removeAnonymousContent];
       let callCounts = {
         insertCalls: [],
         removeCalls: []
@@ -81,33 +82,44 @@ function promiseTestHighlighterOutput(br
           `Remove calls should match for '${word}'.`);
 
         // We reached the amount of calls we expected, so now we can check
         // the amount of rects.
         let lastMaskNode = callCounts.insertCalls.pop();
         if (!lastMaskNode && expectedResult.rectCount !== 0) {
           Assert.ok(false, `No mask node found, but expected ${expectedResult.rectCount} rects.`);
         }
+
         if (lastMaskNode) {
           Assert.equal(lastMaskNode.getElementsByTagName("div").length,
             expectedResult.rectCount, `Amount of inserted rects should match for '${word}'.`);
         }
 
+        // Allow more specific assertions to be tested in `extraTest`.
+        extraTest = eval(extraTest);
+        extraTest(lastMaskNode);
+
         resolve();
       }
 
       // Create a function that will stub the original version and collects
       // the arguments so we can check the results later.
       function stub(which) {
         let prop = which + "Calls";
         return function(node) {
           callCounts[prop].push(node);
           content.clearTimeout(timeout);
           timeout = content.setTimeout(finish, kTimeoutMs);
-          return node;
+          const k = () => {};
+          return {
+            getAttributeForElement: k,
+            removeAttributeForElement: k,
+            setAttributeForElement: k,
+            setTextContentForElement: k
+          };
         };
       }
       document.insertAnonymousContent = stub("insert");
       document.removeAnonymousContent = stub("remove");
     });
   });
 }
 
@@ -190,10 +202,60 @@ add_task(function* testModalSwitching() 
       rectCount: 0,
       insertCalls: 0,
       removeCalls: 0
     };
     promise = promiseTestHighlighterOutput(browser, word, expectedResult);
     findbar.clear();
     yield promiseEnterStringIntoFindField(findbar, word);
     yield promise;
+
+    findbar.close();
+  });
+
+  yield SpecialPowers.pushPrefEnv({ "set": [[ kPrefModalHighlight, true ]] });
+});
+
+// Test if highlighting a dark page is detected properly.
+add_task(function* testDarkPageDetection() {
+  let uri = "https://example.com/browser/toolkit/modules/tests/browser/metadata_simple.html";
+  yield BrowserTestUtils.withNewTab(uri, function* (browser) {
+    let findbar = gBrowser.getFindBar();
+
+    yield promiseOpenFindbar(findbar);
+
+    let word = "ama";
+    let expectedResult = {
+      rectCount: 1,
+      insertCalls: 4,
+      removeCalls: 2
+    };
+
+    let promise = promiseTestHighlighterOutput(browser, word, expectedResult, function(node) {
+      Assert.ok(!node.hasAttribute("brighttext"), "White HTML page shouldn't have 'brighttext' set");
+    });
+    yield promiseEnterStringIntoFindField(findbar, word);
+    yield promise;
+
+    findbar.close();
+  });
+
+  yield BrowserTestUtils.withNewTab("about:mozilla", function* (browser) {
+    let findbar = gBrowser.getFindBar();
+
+    yield promiseOpenFindbar(findbar);
+
+    let word = "mo";
+    let expectedResult = {
+      rectCount: 4,
+      insertCalls: 2,
+      removeCalls: AppConstants.platform == "linux" ? 1 : 2
+    };
+
+    let promise = promiseTestHighlighterOutput(browser, word, expectedResult, node => {
+      Assert.ok(node.hasAttribute("brighttext"), "Dark HTML page should have 'brighttext' set");
+    });
+    yield promiseEnterStringIntoFindField(findbar, word);
+    yield promise;
+
+    findbar.close();
   });
 });