summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/WinUnreadBadge.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base/src/WinUnreadBadge.jsm')
-rw-r--r--comm/mailnews/base/src/WinUnreadBadge.jsm246
1 files changed, 246 insertions, 0 deletions
diff --git a/comm/mailnews/base/src/WinUnreadBadge.jsm b/comm/mailnews/base/src/WinUnreadBadge.jsm
new file mode 100644
index 0000000000..819d8c5719
--- /dev/null
+++ b/comm/mailnews/base/src/WinUnreadBadge.jsm
@@ -0,0 +1,246 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Based on https://github.com/bstreiff/unread-badge.
+ *
+ * Copyright (c) 2013-2020 Brandon Streiff
+ */
+
+const EXPORTED_SYMBOLS = ["WinUnreadBadge"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ imgTools: ["@mozilla.org/image/tools;1", "imgITools"],
+ taskbar: ["@mozilla.org/windows-taskbar;1", "nsIWinTaskbar"],
+});
+
+/**
+ * Get an imgIContainer instance from a canvas element.
+ *
+ * @param {HTMLCanvasElement} canvas - The canvas element.
+ * @param {number} width - The width of the canvas to use.
+ * @param {number} height - The height of the canvas to use.
+ * @returns {imgIContainer}
+ */
+function getCanvasAsImgContainer(canvas, width, height) {
+ let imageData = canvas.getContext("2d").getImageData(0, 0, width, height);
+
+ // Create an imgIEncoder so we can turn the image data into a PNG stream.
+ let imgEncoder = Cc["@mozilla.org/image/encoder;2?type=image/png"].getService(
+ Ci.imgIEncoder
+ );
+ imgEncoder.initFromData(
+ imageData.data,
+ imageData.data.length,
+ imageData.width,
+ imageData.height,
+ imageData.width * 4,
+ imgEncoder.INPUT_FORMAT_RGBA,
+ ""
+ );
+
+ // Now turn the PNG stream into an imgIContainer.
+ let imgBuffer = lazy.NetUtil.readInputStreamToString(
+ imgEncoder,
+ imgEncoder.available()
+ );
+ let iconImage = lazy.imgTools.decodeImageFromBuffer(
+ imgBuffer,
+ imgBuffer.length,
+ "image/png"
+ );
+
+ // Close the PNG stream.
+ imgEncoder.close();
+ return iconImage;
+}
+
+/**
+ * Draw text centered in the middle of a CanvasRenderingContext2D.
+ *
+ * @param {CanvasRenderingContext2D} cxt - The canvas context to operate on.
+ * @param {string} text - The text to draw.
+ */
+function drawUnreadCountText(cxt, text) {
+ cxt.save();
+
+ let imageSize = cxt.canvas.width;
+
+ // Use smaller fonts for longer text to try and squeeze it in.
+ let fontSize = imageSize * (0.95 - 0.15 * text.length);
+
+ cxt.font = "500 " + fontSize + "px Calibri";
+ cxt.fillStyle = "#ffffff";
+ cxt.textAlign = "center";
+
+ // TODO: There isn't a textBaseline for accurate vertical centering ('middle' is the
+ // middle of the 'em block', and digits extend higher than 'm'), and the Mozilla core
+ // does not currently support computation of ascenders and descenters in measureText().
+ // So, we just assume that the font is 70% of the 'px' height we requested, then
+ // compute where the baseline ought to be located.
+ let approximateHeight = fontSize * 0.7;
+
+ cxt.textBaseline = "alphabetic";
+ cxt.fillText(
+ text,
+ imageSize / 2,
+ imageSize - (imageSize - approximateHeight) / 2
+ );
+
+ cxt.restore();
+}
+
+/**
+ * Create a flat badge, as is the Windows 8/10 style.
+ *
+ * @param {HTMLCanvasElement} canvas - The canvas element to draw the badge.
+ * @param {string} text - The text to draw in the badge.
+ */
+function createModernBadgeStyle(canvas, text) {
+ let cxt = canvas.getContext("2d");
+ let iconSize = canvas.width;
+
+ // Draw the background.
+ cxt.save();
+ // Solid color first.
+ cxt.fillStyle = "#ff0039";
+ cxt.shadowOffsetX = 0;
+ cxt.shadowOffsetY = 0;
+ cxt.shadowColor = "rgba(0,0,0,0.7)";
+ cxt.shadowBlur = iconSize / 10;
+ cxt.beginPath();
+ cxt.arc(iconSize / 2, iconSize / 2, iconSize / 2.25, 0, Math.PI * 2, true);
+ cxt.fill();
+ cxt.clip();
+ cxt.closePath();
+ cxt.restore();
+
+ drawUnreadCountText(cxt, text);
+}
+
+/**
+ * Downsample by 4X with simple averaging.
+ *
+ * Drawing at 4X and then downscaling like this gives us better results than
+ * using either CanvasRenderingContext2D.drawImage() to resize or letting
+ * the Windows taskbar service handle the resize, both of which seem to just
+ * give us a simple point resize.
+ *
+ * @param {Window} window - The DOM window.
+ * @param {HTMLCanvasElement} canvas - The input canvas element to resize.
+ * @returns {HTMLCanvasElement} The resized canvas element.
+ */
+function downsampleBy4X(window, canvas) {
+ let resizedCanvas = window.document.createElement("canvas");
+ resizedCanvas.width = resizedCanvas.height = canvas.width / 4;
+ resizedCanvas.style.width = resizedCanvas.style.height =
+ resizedCanvas.width + "px";
+
+ let source = canvas
+ .getContext("2d")
+ .getImageData(0, 0, canvas.width, canvas.height);
+ let downsampled = resizedCanvas
+ .getContext("2d")
+ .createImageData(resizedCanvas.width, resizedCanvas.height);
+
+ for (let y = 0; y < resizedCanvas.height; ++y) {
+ for (let x = 0; x < resizedCanvas.width; ++x) {
+ let r = 0,
+ g = 0,
+ b = 0,
+ a = 0;
+ let index;
+
+ for (let i = 0; i < 4; ++i) {
+ for (let j = 0; j < 4; ++j) {
+ index = ((y * 4 + i) * source.width + (x * 4 + j)) * 4;
+ r += source.data[index];
+ g += source.data[index + 1];
+ b += source.data[index + 2];
+ a += source.data[index + 3];
+ }
+ }
+
+ index = (y * downsampled.width + x) * 4;
+ downsampled.data[index] = Math.round(r / 16);
+ downsampled.data[index + 1] = Math.round(g / 16);
+ downsampled.data[index + 2] = Math.round(b / 16);
+ downsampled.data[index + 3] = Math.round(a / 16);
+ }
+ }
+
+ resizedCanvas.getContext("2d").putImageData(downsampled, 0, 0);
+
+ return resizedCanvas;
+}
+
+/**
+ * A module to manage the unread badge icon on Windows.
+ */
+var WinUnreadBadge = {
+ /**
+ * Keeping an instance of nsITaskbarOverlayIconController alive
+ * to show a taskbar icon after the updateUnreadCount method exits.
+ */
+ _controller: null,
+
+ /**
+ * Update the unread badge.
+ *
+ * @param {number} unreadCount - Unread message count.
+ * @param {number} unreadTooltip - Unread message count tooltip.
+ */
+ async updateUnreadCount(unreadCount, unreadTooltip) {
+ let window = Services.wm.getMostRecentBrowserWindow();
+ if (!window) {
+ return;
+ }
+ if (!this._controller) {
+ this._controller = lazy.taskbar.getOverlayIconController(window.docShell);
+ }
+ if (unreadCount == 0) {
+ // Remove the badge if no unread.
+ this._controller.setOverlayIcon(null, "");
+ return;
+ }
+
+ // Draw the badge in a canvas.
+ let smallIconSize = Cc["@mozilla.org/windows-ui-utils;1"].getService(
+ Ci.nsIWindowsUIUtils
+ ).systemSmallIconSize;
+ let iconSize = Math.floor(
+ (window.windowUtils.displayDPI / 96) * smallIconSize
+ );
+ let iconSize4X = iconSize * 4;
+ let badge = window.document.createElement("canvas");
+ badge.width = badge.height = iconSize4X;
+ badge.style.width = badge.style.height = badge.width + "px";
+
+ createModernBadgeStyle(
+ badge,
+ unreadCount < 100 ? unreadCount.toString() : "99+"
+ );
+
+ badge = downsampleBy4X(window, badge);
+ let icon = getCanvasAsImgContainer(badge, iconSize, iconSize);
+ // Purge image from cache to force encodeImage() to not be lazy
+ icon.requestDiscard();
+ // Side effect of encodeImage() is that it decodes original image
+ lazy.imgTools.encodeImage(icon, "image/png");
+ // Somehow this is needed to prevent NS_ERROR_NOT_AVAILABLE error in
+ // setOverlayIcon.
+ await new Promise(resolve => window.setTimeout(resolve));
+
+ this._controller.setOverlayIcon(icon, unreadTooltip);
+ },
+};