diff options
Diffstat (limited to 'comm/mailnews/base/src/WinUnreadBadge.jsm')
-rw-r--r-- | comm/mailnews/base/src/WinUnreadBadge.jsm | 246 |
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); + }, +}; |