Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#include "PreXULSkeletonUI.h"
#include <algorithm>
#include <dwmapi.h>
#include <math.h>
#include <limits.h>
#include <cmath>
#include <locale>
#include <string>
#include <objbase.h>
#include <shlobj.h>
#include "mozilla/Assertions.h"
#include "mozilla/Attributes.h"
#include "mozilla/BaseProfilerMarkers.h"
#include "mozilla/CacheNtDllThunk.h"
#include "mozilla/FStream.h"
#include "mozilla/GetKnownFolderPath.h"
#include "mozilla/HashFunctions.h"
#include "mozilla/HelperMacros.h"
#include "mozilla/glue/Debug.h"
#include "mozilla/Maybe.h"
#include "mozilla/mscom/ProcessRuntime.h"
#include "mozilla/ResultVariant.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Try.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/UniquePtrExtensions.h"
#include "mozilla/Unused.h"
#include "mozilla/WindowsDpiAwareness.h"
#include "mozilla/WindowsProcessMitigations.h"
namespace mozilla {
// ColorRect defines an optionally-rounded, optionally-bordered rectangle of a
// particular color that we will draw.
struct ColorRect {
uint32_t color;
uint32_t borderColor;
int x;
int y;
int width;
int height;
int borderWidth;
int borderRadius;
bool flipIfRTL;
};
// DrawRect is mostly the same as ColorRect, but exists as an implementation
// detail to simplify drawing borders. We draw borders as a strokeOnly rect
// underneath an inner rect of a particular color. We also need to keep
// track of the backgroundColor for rounding rects, in order to correctly
// anti-alias.
struct DrawRect {
uint32_t color;
uint32_t backgroundColor;
int x;
int y;
int width;
int height;
int borderRadius;
int borderWidth;
bool strokeOnly;
};
struct NormalizedRGB {
double r;
double g;
double b;
};
NormalizedRGB UintToRGB(uint32_t color) {
double r = static_cast<double>(color >> 16 & 0xff) / 255.0;
double g = static_cast<double>(color >> 8 & 0xff) / 255.0;
double b = static_cast<double>(color >> 0 & 0xff) / 255.0;
return NormalizedRGB{r, g, b};
}
uint32_t RGBToUint(const NormalizedRGB& rgb) {
return (static_cast<uint32_t>(rgb.r * 255.0) << 16) |
(static_cast<uint32_t>(rgb.g * 255.0) << 8) |
(static_cast<uint32_t>(rgb.b * 255.0) << 0);
}
double Lerp(double a, double b, double x) { return a + x * (b - a); }
NormalizedRGB Lerp(const NormalizedRGB& a, const NormalizedRGB& b, double x) {
return NormalizedRGB{Lerp(a.r, b.r, x), Lerp(a.g, b.g, x), Lerp(a.b, b.b, x)};
}
// Produces a smooth curve in [0,1] based on a linear input in [0,1]
double SmoothStep3(double x) { return x * x * (3.0 - 2.0 * x); }
struct Margin {
int top = 0;
int right = 0;
int bottom = 0;
int left = 0;
};
static const wchar_t kPreXULSkeletonUIKeyPath[] =
L"SOFTWARE"
L"\\" MOZ_APP_VENDOR L"\\" MOZ_APP_BASENAME L"\\PreXULSkeletonUISettings";
static bool sPreXULSkeletonUIShown = false;
static bool sPreXULSkeletonUIEnabled = false;
static HWND sPreXULSkeletonUIWindow;
static LPWSTR const gStockApplicationIcon = MAKEINTRESOURCEW(32512);
static LPWSTR const gIDCWait = MAKEINTRESOURCEW(32514);
static HANDLE sPreXULSKeletonUIAnimationThread;
static HANDLE sPreXULSKeletonUILockFile = INVALID_HANDLE_VALUE;
static mozilla::mscom::ProcessRuntime* sProcessRuntime;
static uint32_t* sPixelBuffer = nullptr;
static Vector<ColorRect>* sAnimatedRects = nullptr;
static int sTotalChromeHeight = 0;
static volatile LONG sAnimationControlFlag = 0;
static bool sMaximized = false;
static uint32_t sDpi = 0;
// See nsWindow::mNonClientOffset
static Margin sNonClientOffset;
static int sCaptionHeight = 0;
static int sHorizontalResizeMargin = 0;
static int sVerticalResizeMargin = 0;
// See nsWindow::NonClientSizeMargin()
static Margin NonClientSizeMargin() {
return Margin{sCaptionHeight + sVerticalResizeMargin - sNonClientOffset.top,
sHorizontalResizeMargin - sNonClientOffset.right,
sVerticalResizeMargin - sNonClientOffset.bottom,
sHorizontalResizeMargin - sNonClientOffset.left};
}
// Color values needed by the animation loop
static uint32_t sAnimationColor;
static uint32_t sToolbarForegroundColor;
static ThemeMode sTheme = ThemeMode::Invalid;
#define MOZ_DECL_IMPORTED_WIN32_FN(name) \
static decltype(&::name) s##name = nullptr
MOZ_DECL_IMPORTED_WIN32_FN(EnableNonClientDpiScaling);
MOZ_DECL_IMPORTED_WIN32_FN(GetSystemMetricsForDpi);
MOZ_DECL_IMPORTED_WIN32_FN(GetDpiForWindow);
MOZ_DECL_IMPORTED_WIN32_FN(RegisterClassW);
MOZ_DECL_IMPORTED_WIN32_FN(LoadIconW);
MOZ_DECL_IMPORTED_WIN32_FN(LoadCursorW);
MOZ_DECL_IMPORTED_WIN32_FN(CreateWindowExW);
MOZ_DECL_IMPORTED_WIN32_FN(ShowWindow);
MOZ_DECL_IMPORTED_WIN32_FN(SetWindowPos);
MOZ_DECL_IMPORTED_WIN32_FN(GetWindowDC);
MOZ_DECL_IMPORTED_WIN32_FN(GetWindowRect);
MOZ_DECL_IMPORTED_WIN32_FN(MapWindowPoints);
MOZ_DECL_IMPORTED_WIN32_FN(FillRect);
MOZ_DECL_IMPORTED_WIN32_FN(DeleteObject);
MOZ_DECL_IMPORTED_WIN32_FN(ReleaseDC);
MOZ_DECL_IMPORTED_WIN32_FN(MonitorFromWindow);
MOZ_DECL_IMPORTED_WIN32_FN(GetMonitorInfoW);
MOZ_DECL_IMPORTED_WIN32_FN(SetWindowLongPtrW);
MOZ_DECL_IMPORTED_WIN32_FN(StretchDIBits);
MOZ_DECL_IMPORTED_WIN32_FN(CreateSolidBrush);
MOZ_DECL_IMPORTED_WIN32_FN(DwmGetWindowAttribute);
MOZ_DECL_IMPORTED_WIN32_FN(DwmSetWindowAttribute);
#undef MOZ_DECL_IMPORTED_WIN32_FN
static int sWindowWidth;
static int sWindowHeight;
static double sCSSToDevPixelScaling;
static Maybe<PreXULSkeletonUIError> sErrorReason;
static const int kAnimationCSSPixelsPerFrame = 11;
static const int kAnimationCSSExtraWindowSize = 300;
// NOTE: these values were pulled out of thin air as round numbers that are
// likely to be too big to be seen in practice. If we legitimately see windows
// this big, we probably don't want to be drawing them on the CPU anyway.
static const uint32_t kMaxWindowWidth = 1 << 16;
static const uint32_t kMaxWindowHeight = 1 << 16;
static const wchar_t* sEnabledRegSuffix = L"|Enabled";
static const wchar_t* sScreenXRegSuffix = L"|ScreenX";
static const wchar_t* sScreenYRegSuffix = L"|ScreenY";
static const wchar_t* sWidthRegSuffix = L"|Width";
static const wchar_t* sHeightRegSuffix = L"|Height";
static const wchar_t* sMaximizedRegSuffix = L"|Maximized";
static const wchar_t* sUrlbarCSSRegSuffix = L"|UrlbarCSSSpan";
static const wchar_t* sCssToDevPixelScalingRegSuffix = L"|CssToDevPixelScaling";
static const wchar_t* sSearchbarRegSuffix = L"|SearchbarCSSSpan";
static const wchar_t* sSpringsCSSRegSuffix = L"|SpringsCSSSpan";
static const wchar_t* sThemeRegSuffix = L"|Theme";
static const wchar_t* sFlagsRegSuffix = L"|Flags";
static const wchar_t* sProgressSuffix = L"|Progress";
std::wstring GetRegValueName(const wchar_t* prefix, const wchar_t* suffix) {
std::wstring result(prefix);
result.append(suffix);
return result;
}
// This is paraphrased from WinHeaderOnlyUtils.h. The fact that this file is
// included in standalone SpiderMonkey builds prohibits us from including that
// file directly, and it hardly warrants its own header. Bug 1674920 tracks
// only including this file for gecko-related builds.
Result<UniquePtr<wchar_t[]>, PreXULSkeletonUIError> GetBinaryPath() {
DWORD bufLen = MAX_PATH;
UniquePtr<wchar_t[]> buf;
while (true) {
buf = MakeUnique<wchar_t[]>(bufLen);
DWORD retLen = ::GetModuleFileNameW(nullptr, buf.get(), bufLen);
if (!retLen) {
return Err(PreXULSkeletonUIError::FilesystemFailure);
}
if (retLen == bufLen && ::GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
bufLen *= 2;
continue;
}
break;
}
return buf;
}
// PreXULSkeletonUIDisallowed means that we don't even have the capacity to
// enable the skeleton UI, whether because we're on a platform that doesn't
// support it or because we launched with command line arguments that we don't
// support. Some of these situations are transient, so we want to make sure we
// don't mess with registry values in these scenarios that we may use in
// other scenarios in which the skeleton UI is actually enabled.
static bool PreXULSkeletonUIDisallowed() {
return sErrorReason.isSome() &&
(*sErrorReason == PreXULSkeletonUIError::Cmdline ||
*sErrorReason == PreXULSkeletonUIError::EnvVars);
}
// Note: this is specifically *not* a robust, multi-locale lowercasing
// operation. It is not intended to be such. It is simply intended to match the
// way in which we look for other instances of firefox to remote into.
// See
static void MutateStringToLowercase(wchar_t* ptr) {
while (*ptr) {
wchar_t ch = *ptr;
if (ch >= L'A' && ch <= L'Z') {
*ptr = ch + (L'a' - L'A');
}
++ptr;
}
}
static Result<Ok, PreXULSkeletonUIError> GetSkeletonUILock() {
auto localAppDataPath = GetKnownFolderPath(FOLDERID_LocalAppData);
if (!localAppDataPath) {
return Err(PreXULSkeletonUIError::FilesystemFailure);
}
if (sPreXULSKeletonUILockFile != INVALID_HANDLE_VALUE) {
return Ok();
}
// Note: because we're in mozglue, we cannot easily access things from
// toolkit, like `GetInstallHash`. We could move `GetInstallHash` into
// mozglue, and rip out all of its usage of types defined in toolkit headers.
// However, it seems cleaner to just hash the bin path ourselves. We don't
// get quite the same robustness that `GetInstallHash` might provide, but
// we already don't have that with how we key our registry values, so it
// probably makes sense to just match those.
UniquePtr<wchar_t[]> binPath;
MOZ_TRY_VAR(binPath, GetBinaryPath());
// Lowercase the binpath to match how we look for remote instances.
MutateStringToLowercase(binPath.get());
// The number of bytes * 2 characters per byte + 1 for the null terminator
uint32_t hexHashSize = sizeof(uint32_t) * 2 + 1;
UniquePtr<wchar_t[]> installHash = MakeUnique<wchar_t[]>(hexHashSize);
// This isn't perfect - it's a 32-bit hash of the path to our executable. It
// could reasonably collide, or casing could potentially affect things, but
// the theory is that that should be uncommon enough and the failure case
// mild enough that this is fine.
uint32_t binPathHash = HashString(binPath.get());
swprintf(installHash.get(), hexHashSize, L"%08x", binPathHash);
std::wstring lockFilePath;
lockFilePath.append(localAppDataPath.get());
lockFilePath.append(
L"\\" MOZ_APP_VENDOR L"\\" MOZ_APP_BASENAME L"\\SkeletonUILock-");
lockFilePath.append(installHash.get());
// We intentionally leak this file - that is okay, and (kind of) the point.
// We want to hold onto this handle until the application exits, and hold
// onto it with exclusive rights. If this check fails, then we assume that
// another instance of the executable is holding it, and thus return false.
sPreXULSKeletonUILockFile =
::CreateFileW(lockFilePath.c_str(), GENERIC_READ | GENERIC_WRITE,
0, // No sharing - this is how the lock works
nullptr, CREATE_ALWAYS,
FILE_FLAG_DELETE_ON_CLOSE, // Don't leave this lying around
nullptr);
if (sPreXULSKeletonUILockFile == INVALID_HANDLE_VALUE) {
return Err(PreXULSkeletonUIError::FailedGettingLock);
}
return Ok();
}
const char kGeneralSection[] = "[General]";
const char kStartWithLastProfile[] = "StartWithLastProfile=";
static bool ProfileDbHasStartWithLastProfile(IFStream& iniContents) {
bool inGeneral = false;
std::string line;
while (std::getline(iniContents, line)) {
size_t whitespace = 0;
while (line.length() > whitespace &&
(line[whitespace] == ' ' || line[whitespace] == '\t')) {
whitespace++;
}
line.erase(0, whitespace);
if (line.compare(kGeneralSection) == 0) {
inGeneral = true;
} else if (inGeneral) {
if (line[0] == '[') {
inGeneral = false;
} else {
if (line.find(kStartWithLastProfile) == 0) {
char val = line.c_str()[sizeof(kStartWithLastProfile) - 1];
if (val == '0') {
return false;
} else if (val == '1') {
return true;
}
}
}
}
}
// If we don't find it in the .ini file, we interpret that as true
return true;
}
static Result<Ok, PreXULSkeletonUIError> CheckForStartWithLastProfile() {
auto roamingAppData = GetKnownFolderPath(FOLDERID_RoamingAppData);
if (!roamingAppData) {
return Err(PreXULSkeletonUIError::FilesystemFailure);
}
std::wstring profileDbPath(roamingAppData.get());
profileDbPath.append(
L"\\" MOZ_APP_VENDOR L"\\" MOZ_APP_BASENAME L"\\profiles.ini");
IFStream profileDb(profileDbPath.c_str());
if (profileDb.fail()) {
return Err(PreXULSkeletonUIError::FilesystemFailure);
}
if (!ProfileDbHasStartWithLastProfile(profileDb)) {
return Err(PreXULSkeletonUIError::NoStartWithLastProfile);
}
return Ok();
}
// We could use nsAutoRegKey, but including nsWindowsHelpers.h causes build
// failures in random places because we're in mozglue. Overall it should be
// simpler and cleaner to just step around that issue with this class:
class MOZ_RAII AutoCloseRegKey {
public:
explicit AutoCloseRegKey(HKEY key) : mKey(key) {}
~AutoCloseRegKey() { ::RegCloseKey(mKey); }
private:
HKEY mKey;
};
int CSSToDevPixels(double cssPixels, double scaling) {
return floor(cssPixels * scaling + 0.5);
}
int CSSToDevPixels(int cssPixels, double scaling) {
return CSSToDevPixels((double)cssPixels, scaling);
}
int CSSToDevPixelsFloor(double cssPixels, double scaling) {
return floor(cssPixels * scaling);
}
// Some things appear to floor to device pixels rather than rounding. A good
// example of this is border widths.
int CSSToDevPixelsFloor(int cssPixels, double scaling) {
return CSSToDevPixelsFloor((double)cssPixels, scaling);
}
double SignedDistanceToCircle(double x, double y, double radius) {
return sqrt(x * x + y * y) - radius;
}
// For more details, see
// which was a reference for this function.
double DistanceAntiAlias(double signedDistance) {
// Distance assumed to be in device pixels. We use an aa range of 0.5 for
// reasons detailed in the linked code above.
const double aaRange = 0.5;
double dist = 0.5 * signedDistance / aaRange;
if (dist <= -0.5 + std::numeric_limits<double>::epsilon()) return 1.0;
if (dist >= 0.5 - std::numeric_limits<double>::epsilon()) return 0.0;
return 0.5 + dist * (0.8431027 * dist * dist - 1.14453603);
}
void RasterizeRoundedRectTopAndBottom(const DrawRect& rect) {
if (rect.height <= 2 * rect.borderRadius) {
MOZ_ASSERT(false, "Skeleton UI rect height too small for border radius.");
return;
}
if (rect.width <= 2 * rect.borderRadius) {
MOZ_ASSERT(false, "Skeleton UI rect width too small for border radius.");
return;
}
NormalizedRGB rgbBase = UintToRGB(rect.backgroundColor);
NormalizedRGB rgbBlend = UintToRGB(rect.color);
for (int rowIndex = 0; rowIndex < rect.borderRadius; ++rowIndex) {
int yTop = rect.y + rect.borderRadius - 1 - rowIndex;
int yBottom = rect.y + rect.height - rect.borderRadius + rowIndex;
uint32_t* lineStartTop = &sPixelBuffer[yTop * sWindowWidth];
uint32_t* innermostPixelTopLeft =
lineStartTop + rect.x + rect.borderRadius - 1;
uint32_t* innermostPixelTopRight =
lineStartTop + rect.x + rect.width - rect.borderRadius;
uint32_t* lineStartBottom = &sPixelBuffer[yBottom * sWindowWidth];
uint32_t* innermostPixelBottomLeft =
lineStartBottom + rect.x + rect.borderRadius - 1;
uint32_t* innermostPixelBottomRight =
lineStartBottom + rect.x + rect.width - rect.borderRadius;
// Add 0.5 to x and y to get the pixel center.
double pixelY = (double)rowIndex + 0.5;
for (int columnIndex = 0; columnIndex < rect.borderRadius; ++columnIndex) {
double pixelX = (double)columnIndex + 0.5;
double distance =
SignedDistanceToCircle(pixelX, pixelY, (double)rect.borderRadius);
double alpha = DistanceAntiAlias(distance);
NormalizedRGB rgb = Lerp(rgbBase, rgbBlend, alpha);
uint32_t color = RGBToUint(rgb);
innermostPixelTopLeft[-columnIndex] = color;
innermostPixelTopRight[columnIndex] = color;
innermostPixelBottomLeft[-columnIndex] = color;
innermostPixelBottomRight[columnIndex] = color;
}
std::fill(innermostPixelTopLeft + 1, innermostPixelTopRight, rect.color);
std::fill(innermostPixelBottomLeft + 1, innermostPixelBottomRight,
rect.color);
}
}
void RasterizeAnimatedRoundedRectTopAndBottom(
const ColorRect& colorRect, const uint32_t* animationLookup,
int priorUpdateAreaMin, int priorUpdateAreaMax, int currentUpdateAreaMin,
int currentUpdateAreaMax, int animationMin) {
// We iterate through logical pixel rows here, from inside to outside, which
// for the top of the rounded rect means from bottom to top, and for the
// bottom of the rect means top to bottom. We paint pixels from left to
// right on the top and bottom rows at the same time for the entire animation
// window. (If the animation window does not overlap any rounded corners,
// however, we won't be called at all)
for (int rowIndex = 0; rowIndex < colorRect.borderRadius; ++rowIndex) {
int yTop = colorRect.y + colorRect.borderRadius - 1 - rowIndex;
int yBottom =
colorRect.y + colorRect.height - colorRect.borderRadius + rowIndex;
uint32_t* lineStartTop = &sPixelBuffer[yTop * sWindowWidth];
uint32_t* lineStartBottom = &sPixelBuffer[yBottom * sWindowWidth];
// Add 0.5 to x and y to get the pixel center.
double pixelY = (double)rowIndex + 0.5;
for (int x = priorUpdateAreaMin; x < currentUpdateAreaMax; ++x) {
// The column index is the distance from the innermost pixel, which
// is different depending on whether we're on the left or right
// side of the rect. It will always be the max here, and if it's
// negative that just means we're outside the rounded area.
int columnIndex =
std::max((int)colorRect.x + (int)colorRect.borderRadius - x - 1,
x - ((int)colorRect.x + (int)colorRect.width -
(int)colorRect.borderRadius));
double alpha = 1.0;
if (columnIndex >= 0) {
double pixelX = (double)columnIndex + 0.5;
double distance = SignedDistanceToCircle(
pixelX, pixelY, (double)colorRect.borderRadius);
alpha = DistanceAntiAlias(distance);
}
// We don't do alpha blending for the antialiased pixels at the
// shape's border. It is not noticeable in the animation.
if (alpha > 1.0 - std::numeric_limits<double>::epsilon()) {
// Overwrite the tail end of last frame's animation with the
// rect's normal, unanimated color.
uint32_t color = x < priorUpdateAreaMax
? colorRect.color
: animationLookup[x - animationMin];
lineStartTop[x] = color;
lineStartBottom[x] = color;
}
}
}
}
void RasterizeColorRect(const ColorRect& colorRect) {
// We sometimes split our rect into two, to simplify drawing borders. If we
// have a border, we draw a stroke-only rect first, and then draw the smaller
// inner rect on top of it.
Vector<DrawRect, 2> drawRects;
Unused << drawRects.reserve(2);
if (colorRect.borderWidth == 0) {
DrawRect rect = {};
rect.color = colorRect.color;
rect.backgroundColor =
sPixelBuffer[colorRect.y * sWindowWidth + colorRect.x];
rect.x = colorRect.x;
rect.y = colorRect.y;
rect.width = colorRect.width;
rect.height = colorRect.height;
rect.borderRadius = colorRect.borderRadius;
rect.strokeOnly = false;
drawRects.infallibleAppend(rect);
} else {
DrawRect borderRect = {};
borderRect.color = colorRect.borderColor;
borderRect.backgroundColor =
sPixelBuffer[colorRect.y * sWindowWidth + colorRect.x];
borderRect.x = colorRect.x;
borderRect.y = colorRect.y;
borderRect.width = colorRect.width;
borderRect.height = colorRect.height;
borderRect.borderRadius = colorRect.borderRadius;
borderRect.borderWidth = colorRect.borderWidth;
borderRect.strokeOnly = true;
drawRects.infallibleAppend(borderRect);
DrawRect baseRect = {};
baseRect.color = colorRect.color;
baseRect.backgroundColor = borderRect.color;
baseRect.x = colorRect.x + colorRect.borderWidth;
baseRect.y = colorRect.y + colorRect.borderWidth;
baseRect.width = colorRect.width - 2 * colorRect.borderWidth;
baseRect.height = colorRect.height - 2 * colorRect.borderWidth;
baseRect.borderRadius =
std::max(0, (int)colorRect.borderRadius - (int)colorRect.borderWidth);
baseRect.borderWidth = 0;
baseRect.strokeOnly = false;
drawRects.infallibleAppend(baseRect);
}
for (const DrawRect& rect : drawRects) {
if (rect.height <= 0 || rect.width <= 0) {
continue;
}
// For rounded rectangles, the first thing we do is draw the top and
// bottom of the rectangle, with the more complicated logic below. After
// that we can just draw the vertically centered part of the rect like
// normal.
RasterizeRoundedRectTopAndBottom(rect);
// We then draw the flat, central portion of the rect (which in the case of
// non-rounded rects, is just the entire thing.)
int solidRectStartY =
std::clamp(rect.y + rect.borderRadius, 0, sTotalChromeHeight);
int solidRectEndY = std::clamp(rect.y + rect.height - rect.borderRadius, 0,
sTotalChromeHeight);
for (int y = solidRectStartY; y < solidRectEndY; ++y) {
// For strokeOnly rects (used to draw borders), we just draw the left
// and right side here. Looping down a column of pixels is not the most
// cache-friendly thing, but it shouldn't be a big deal given the height
// of the urlbar.
// Also, if borderRadius is less than borderWidth, we need to ensure
// that we fully draw the top and bottom lines, so we make sure to check
// that we're inside the middle range range before excluding pixels.
if (rect.strokeOnly && y - rect.y > rect.borderWidth &&
rect.y + rect.height - y > rect.borderWidth) {
int startXLeft = std::clamp(rect.x, 0, sWindowWidth);
int endXLeft = std::clamp(rect.x + rect.borderWidth, 0, sWindowWidth);
int startXRight =
std::clamp(rect.x + rect.width - rect.borderWidth, 0, sWindowWidth);
int endXRight = std::clamp(rect.x + rect.width, 0, sWindowWidth);
uint32_t* lineStart = &sPixelBuffer[y * sWindowWidth];
uint32_t* dataStartLeft = lineStart + startXLeft;
uint32_t* dataEndLeft = lineStart + endXLeft;
uint32_t* dataStartRight = lineStart + startXRight;
uint32_t* dataEndRight = lineStart + endXRight;
std::fill(dataStartLeft, dataEndLeft, rect.color);
std::fill(dataStartRight, dataEndRight, rect.color);
} else {
int startX = std::clamp(rect.x, 0, sWindowWidth);
int endX = std::clamp(rect.x + rect.width, 0, sWindowWidth);
uint32_t* lineStart = &sPixelBuffer[y * sWindowWidth];
uint32_t* dataStart = lineStart + startX;
uint32_t* dataEnd = lineStart + endX;
std::fill(dataStart, dataEnd, rect.color);
}
}
}
}
// Paints the pixels to sPixelBuffer for the skeleton UI animation (a light
// gradient which moves from left to right across the grey placeholder rects).
// Takes in the rect to draw, together with a lookup table for the gradient,
// and the bounds of the previous and current frame of the animation.
bool RasterizeAnimatedRect(const ColorRect& colorRect,
const uint32_t* animationLookup,
int priorAnimationMin, int animationMin,
int animationMax) {
int rectMin = colorRect.x;
int rectMax = colorRect.x + colorRect.width;
bool animationWindowOverlaps =
rectMax >= priorAnimationMin && rectMin < animationMax;
int priorUpdateAreaMin = std::max(rectMin, priorAnimationMin);
int priorUpdateAreaMax = std::min(rectMax, animationMin);
int currentUpdateAreaMin = std::max(rectMin, animationMin);
int currentUpdateAreaMax = std::min(rectMax, animationMax);
if (!animationWindowOverlaps) {
return false;
}
bool animationWindowOverlapsBorderRadius =
rectMin + colorRect.borderRadius > priorAnimationMin ||
rectMax - colorRect.borderRadius <= animationMax;
// If we don't overlap the left or right side of the rounded rectangle,
// just pretend it's not rounded. This is a small optimization but
// there's no point in doing all of this rounded rectangle checking if
// we aren't even overlapping
int borderRadius =
animationWindowOverlapsBorderRadius ? colorRect.borderRadius : 0;
if (borderRadius > 0) {
// Similarly to how we draw the rounded rects in DrawSkeletonUI, we
// first draw the rounded top and bottom, and then we draw the center
// rect.
RasterizeAnimatedRoundedRectTopAndBottom(
colorRect, animationLookup, priorUpdateAreaMin, priorUpdateAreaMax,
currentUpdateAreaMin, currentUpdateAreaMax, animationMin);
}
for (int y = colorRect.y + borderRadius;
y < colorRect.y + colorRect.height - borderRadius; ++y) {
uint32_t* lineStart = &sPixelBuffer[y * sWindowWidth];
// Overwrite the tail end of last frame's animation with the rect's
// normal, unanimated color.
for (int x = priorUpdateAreaMin; x < priorUpdateAreaMax; ++x) {
lineStart[x] = colorRect.color;
}
// Then apply the animated color
for (int x = currentUpdateAreaMin; x < currentUpdateAreaMax; ++x) {
lineStart[x] = animationLookup[x - animationMin];
}
}
return true;
}
bool FillRectWithColor(HDC hdc, LPCRECT rect, uint32_t mozColor) {
HBRUSH brush = sCreateSolidBrush(RGB((mozColor & 0xff0000) >> 16,
(mozColor & 0x00ff00) >> 8,
(mozColor & 0x0000ff) >> 0));
int fillRectResult = sFillRect(hdc, rect, brush);
sDeleteObject(brush);
return !!fillRectResult;
}
Result<Ok, PreXULSkeletonUIError> DrawSkeletonUI(
HWND hWnd, CSSPixelSpan urlbarCSSSpan, CSSPixelSpan searchbarCSSSpan,
Vector<CSSPixelSpan>& springs, const ThemeColors& currentTheme,
const EnumSet<SkeletonUIFlag, uint32_t>& flags) {
// NOTE: we opt here to paint a pixel buffer for the application chrome by
// hand, without using native UI library methods. Why do we do this?
//
// 1) It gives us a little bit more control, especially if we want to animate
// any of this.
// 2) It's actually more portable. We can do this on any platform where we
// can blit a pixel buffer to the screen, and it only has to change
// insofar as the UI is different on those platforms (and thus would have
// to change anyway.)
//
// The performance impact of this ought to be negligible. As far as has been
// observed, on slow reference hardware this might take up to a millisecond,
// for a startup which otherwise takes 30 seconds.
//
// The readability and maintainability are a greater concern. When the
// silhouette of Firefox's core UI changes, this code will likely need to
// change. However, for the foreseeable future, our skeleton UI will be mostly
// axis-aligned geometric shapes, and the thought is that any code which is
// manipulating raw pixels should not be *too* hard to maintain and
// understand so long as it is only painting such simple shapes.
sAnimationColor = currentTheme.animationColor;
sToolbarForegroundColor = currentTheme.toolbarForegroundColor;
bool menubarShown = flags.contains(SkeletonUIFlag::MenubarShown);
bool verticalTabs = flags.contains(SkeletonUIFlag::VerticalTabs);
bool bookmarksToolbarShown =
flags.contains(SkeletonUIFlag::BookmarksToolbarShown);
bool rtlEnabled = flags.contains(SkeletonUIFlag::RtlEnabled);
int chromeHorMargin = CSSToDevPixels(2, sCSSToDevPixelScaling);
int verticalOffset = sMaximized ? sVerticalResizeMargin : 0;
int horizontalOffset =
sHorizontalResizeMargin - (sMaximized ? 0 : chromeHorMargin);
// found in tabs.inc.css, "--tab-min-height" + 2 * "--tab-block-margin"
int tabBarHeight =
verticalTabs ? 0 : CSSToDevPixels(44, sCSSToDevPixelScaling);
int selectedTabBorderWidth = CSSToDevPixels(2, sCSSToDevPixelScaling);
// found in tabs.inc.css, "--tab-block-margin"
int titlebarSpacerWidth = horizontalOffset +
CSSToDevPixels(2, sCSSToDevPixelScaling) -
selectedTabBorderWidth;
if (!sMaximized && !menubarShown) {
// found in tabs.inc.css, ".titlebar-spacer"
titlebarSpacerWidth += CSSToDevPixels(40, sCSSToDevPixelScaling);
}
// found in tabs.inc.css, "--tab-block-margin"
int selectedTabMarginTop =
CSSToDevPixels(4, sCSSToDevPixelScaling) - selectedTabBorderWidth;
int selectedTabMarginBottom =
CSSToDevPixels(4, sCSSToDevPixelScaling) - selectedTabBorderWidth;
int selectedTabBorderRadius = CSSToDevPixels(4, sCSSToDevPixelScaling);
int selectedTabWidth =
CSSToDevPixels(221, sCSSToDevPixelScaling) + 2 * selectedTabBorderWidth;
int toolbarHeight = CSSToDevPixels(40, sCSSToDevPixelScaling);
// found in browser.css, "#PersonalToolbar"
int bookmarkToolbarHeight = CSSToDevPixels(28, sCSSToDevPixelScaling);
if (bookmarksToolbarShown) {
toolbarHeight += bookmarkToolbarHeight;
}
// found in urlbar-searchbar.inc.css, "#urlbar[breakout]"
int urlbarTopOffset = CSSToDevPixels(4, sCSSToDevPixelScaling);
int urlbarHeight = CSSToDevPixels(32, sCSSToDevPixelScaling);
// found in browser-aero.css, "#navigator-toolbox::after" border-bottom
int chromeContentDividerHeight = CSSToDevPixels(1, sCSSToDevPixelScaling);
int tabPlaceholderBarMarginTop = CSSToDevPixels(14, sCSSToDevPixelScaling);
int tabPlaceholderBarMarginLeft = CSSToDevPixels(10, sCSSToDevPixelScaling);
int tabPlaceholderBarHeight = CSSToDevPixels(10, sCSSToDevPixelScaling);
int tabPlaceholderBarWidth = CSSToDevPixels(120, sCSSToDevPixelScaling);
int toolbarPlaceholderHeight = CSSToDevPixels(10, sCSSToDevPixelScaling);
int toolbarPlaceholderMarginRight =
rtlEnabled ? CSSToDevPixels(11, sCSSToDevPixelScaling)
: CSSToDevPixels(9, sCSSToDevPixelScaling);
int toolbarPlaceholderMarginLeft =
rtlEnabled ? CSSToDevPixels(9, sCSSToDevPixelScaling)
: CSSToDevPixels(11, sCSSToDevPixelScaling);
int placeholderMargin = CSSToDevPixels(8, sCSSToDevPixelScaling);
int menubarHeightDevPixels =
menubarShown ? CSSToDevPixels(28, sCSSToDevPixelScaling) : 0;
// defined in urlbar-searchbar.inc.css as --urlbar-margin-inline: 5px
int urlbarMargin =
CSSToDevPixels(5, sCSSToDevPixelScaling) + horizontalOffset;
int urlbarTextPlaceholderMarginTop =
CSSToDevPixels(12, sCSSToDevPixelScaling);
int urlbarTextPlaceholderMarginLeft =
CSSToDevPixels(12, sCSSToDevPixelScaling);
int urlbarTextPlaceHolderWidth = CSSToDevPixels(
std::clamp(urlbarCSSSpan.end - urlbarCSSSpan.start - 10.0, 0.0, 260.0),
sCSSToDevPixelScaling);
int urlbarTextPlaceholderHeight = CSSToDevPixels(10, sCSSToDevPixelScaling);
int searchbarTextPlaceholderWidth = CSSToDevPixels(62, sCSSToDevPixelScaling);
auto scopeExit = MakeScopeExit([&] {
delete sAnimatedRects;
sAnimatedRects = nullptr;
});
Vector<ColorRect> rects;
ColorRect menubar = {};
menubar.color = currentTheme.titlebarColor;
menubar.x = 0;
menubar.y = verticalOffset;
menubar.width = sWindowWidth;
menubar.height = menubarHeightDevPixels;
menubar.flipIfRTL = false;
if (!rects.append(menubar)) {
return Err(PreXULSkeletonUIError::OOM);
}
int placeholderBorderRadius = CSSToDevPixels(4, sCSSToDevPixelScaling);
// found in browser.css "--toolbarbutton-border-radius"
int urlbarBorderRadius = CSSToDevPixels(4, sCSSToDevPixelScaling);
// The (traditionally dark blue on Windows) background of the tab bar.
ColorRect tabBar = {};
tabBar.color = currentTheme.titlebarColor;
tabBar.x = 0;
tabBar.y = menubar.y + menubar.height;
tabBar.width = sWindowWidth;
tabBar.height = tabBarHeight;
tabBar.flipIfRTL = false;
if (!rects.append(tabBar)) {
return Err(PreXULSkeletonUIError::OOM);
}
if (!verticalTabs) {
// The initial selected tab
ColorRect selectedTab = {};
selectedTab.color = currentTheme.tabColor;
selectedTab.x = titlebarSpacerWidth;
selectedTab.y = menubar.y + menubar.height + selectedTabMarginTop;
selectedTab.width = selectedTabWidth;
selectedTab.height =
tabBar.y + tabBar.height - selectedTab.y - selectedTabMarginBottom;
selectedTab.borderColor = currentTheme.tabOutlineColor;
selectedTab.borderWidth = selectedTabBorderWidth;
selectedTab.borderRadius = selectedTabBorderRadius;
selectedTab.flipIfRTL = true;
if (!rects.append(selectedTab)) {
return Err(PreXULSkeletonUIError::OOM);
}
// A placeholder rect representing text that will fill the selected tab
// title
ColorRect tabTextPlaceholder = {};
tabTextPlaceholder.color = currentTheme.toolbarForegroundColor;
tabTextPlaceholder.x = selectedTab.x + tabPlaceholderBarMarginLeft;
tabTextPlaceholder.y = selectedTab.y + tabPlaceholderBarMarginTop;
tabTextPlaceholder.width = tabPlaceholderBarWidth;
tabTextPlaceholder.height = tabPlaceholderBarHeight;
tabTextPlaceholder.borderRadius = placeholderBorderRadius;
tabTextPlaceholder.flipIfRTL = true;
if (!rects.append(tabTextPlaceholder)) {
return Err(PreXULSkeletonUIError::OOM);
}
if (!sAnimatedRects->append(tabTextPlaceholder)) {
return Err(PreXULSkeletonUIError::OOM);
}
}
// The toolbar background
ColorRect toolbar = {};
// In the vertical tabs case the main toolbar is in the titlebar:
toolbar.color =
verticalTabs ? currentTheme.titlebarColor : currentTheme.backgroundColor;
toolbar.x = 0;
toolbar.y = tabBar.y + tabBarHeight;
toolbar.width = sWindowWidth;
toolbar.height = toolbarHeight;
toolbar.flipIfRTL = false;
if (!rects.append(toolbar)) {
return Err(PreXULSkeletonUIError::OOM);
}
// The single-pixel divider line below the toolbar
ColorRect chromeContentDivider = {};
chromeContentDivider.color = currentTheme.chromeContentDividerColor;
chromeContentDivider.x = 0;
chromeContentDivider.y = toolbar.y + toolbar.height;
chromeContentDivider.width = sWindowWidth;
chromeContentDivider.height = chromeContentDividerHeight;
chromeContentDivider.flipIfRTL = false;
if (!rects.append(chromeContentDivider)) {
return Err(PreXULSkeletonUIError::OOM);
}
// The urlbar
ColorRect urlbar = {};
urlbar.color = currentTheme.urlbarColor;
urlbar.x = CSSToDevPixels(urlbarCSSSpan.start, sCSSToDevPixelScaling) +
horizontalOffset;
urlbar.y = tabBar.y + tabBarHeight + urlbarTopOffset;
urlbar.width = CSSToDevPixels((urlbarCSSSpan.end - urlbarCSSSpan.start),
sCSSToDevPixelScaling);
urlbar.height = urlbarHeight;
urlbar.borderColor = currentTheme.urlbarBorderColor;
urlbar.borderWidth = CSSToDevPixels(1, sCSSToDevPixelScaling);
urlbar.borderRadius = urlbarBorderRadius;
urlbar.flipIfRTL = false;
if (!rects.append(urlbar)) {
return Err(PreXULSkeletonUIError::OOM);
}
// The urlbar placeholder rect representating text that will fill the urlbar
// If rtl is enabled, it is flipped relative to the the urlbar rectangle, not
// sWindowWidth.
ColorRect urlbarTextPlaceholder = {};
urlbarTextPlaceholder.color = currentTheme.toolbarForegroundColor;
urlbarTextPlaceholder.x =
rtlEnabled
? ((urlbar.x + urlbar.width) - urlbarTextPlaceholderMarginLeft -
urlbarTextPlaceHolderWidth)
: (urlbar.x + urlbarTextPlaceholderMarginLeft);
urlbarTextPlaceholder.y = urlbar.y + urlbarTextPlaceholderMarginTop;
urlbarTextPlaceholder.width = urlbarTextPlaceHolderWidth;
urlbarTextPlaceholder.height = urlbarTextPlaceholderHeight;
urlbarTextPlaceholder.borderRadius = placeholderBorderRadius;
urlbarTextPlaceholder.flipIfRTL = false;
if (!rects.append(urlbarTextPlaceholder)) {
return Err(PreXULSkeletonUIError::OOM);
}
// The searchbar and placeholder text, if present
// This is y-aligned with the urlbar
bool hasSearchbar = searchbarCSSSpan.start != 0 && searchbarCSSSpan.end != 0;
ColorRect searchbarRect = {};
if (hasSearchbar == true) {
searchbarRect.color = currentTheme.urlbarColor;
searchbarRect.x =
CSSToDevPixels(searchbarCSSSpan.start, sCSSToDevPixelScaling) +
horizontalOffset;
searchbarRect.y = urlbar.y;
searchbarRect.width = CSSToDevPixels(
searchbarCSSSpan.end - searchbarCSSSpan.start, sCSSToDevPixelScaling);
searchbarRect.height = urlbarHeight;
searchbarRect.borderRadius = urlbarBorderRadius;
searchbarRect.borderColor = currentTheme.urlbarBorderColor;
searchbarRect.borderWidth = CSSToDevPixels(1, sCSSToDevPixelScaling);
searchbarRect.flipIfRTL = false;
if (!rects.append(searchbarRect)) {
return Err(PreXULSkeletonUIError::OOM);
}
// The placeholder rect representating text that will fill the searchbar
// This uses the same margins as the urlbarTextPlaceholder
// If rtl is enabled, it is flipped relative to the the searchbar rectangle,
// not sWindowWidth.
ColorRect searchbarTextPlaceholder = {};
searchbarTextPlaceholder.color = currentTheme.toolbarForegroundColor;
searchbarTextPlaceholder.x =
rtlEnabled
? ((searchbarRect.x + searchbarRect.width) -
urlbarTextPlaceholderMarginLeft - searchbarTextPlaceholderWidth)
: (searchbarRect.x + urlbarTextPlaceholderMarginLeft);
searchbarTextPlaceholder.y =
searchbarRect.y + urlbarTextPlaceholderMarginTop;
searchbarTextPlaceholder.width = searchbarTextPlaceholderWidth;
searchbarTextPlaceholder.height = urlbarTextPlaceholderHeight;
searchbarTextPlaceholder.flipIfRTL = false;
if (!rects.append(searchbarTextPlaceholder) ||
!sAnimatedRects->append(searchbarTextPlaceholder)) {
return Err(PreXULSkeletonUIError::OOM);
}
}
// Determine where the placeholder rectangles should not go. This is
// anywhere occupied by a spring, urlbar, or searchbar
Vector<DevPixelSpan> noPlaceholderSpans;
DevPixelSpan urlbarSpan;
urlbarSpan.start = urlbar.x - urlbarMargin;
urlbarSpan.end = urlbar.width + urlbar.x + urlbarMargin;
DevPixelSpan searchbarSpan;
if (hasSearchbar) {
searchbarSpan.start = searchbarRect.x - urlbarMargin;
searchbarSpan.end = searchbarRect.width + searchbarRect.x + urlbarMargin;
}
DevPixelSpan marginLeftPlaceholder;
marginLeftPlaceholder.start = toolbarPlaceholderMarginLeft;
marginLeftPlaceholder.end = toolbarPlaceholderMarginLeft;
if (!noPlaceholderSpans.append(marginLeftPlaceholder)) {
return Err(PreXULSkeletonUIError::OOM);
}
if (rtlEnabled) {
// If we're RTL, then the springs as ordered in the DOM will be from right
// to left, which will break our comparison logic below
springs.reverse();
}
for (auto spring : springs) {
DevPixelSpan springDevPixels;
springDevPixels.start =
CSSToDevPixels(spring.start, sCSSToDevPixelScaling) + horizontalOffset;
springDevPixels.end =
CSSToDevPixels(spring.end, sCSSToDevPixelScaling) + horizontalOffset;
if (!noPlaceholderSpans.append(springDevPixels)) {
return Err(PreXULSkeletonUIError::OOM);
}
}
DevPixelSpan marginRightPlaceholder;
marginRightPlaceholder.start = sWindowWidth - toolbarPlaceholderMarginRight;
marginRightPlaceholder.end = sWindowWidth - toolbarPlaceholderMarginRight;
if (!noPlaceholderSpans.append(marginRightPlaceholder)) {
return Err(PreXULSkeletonUIError::OOM);
}
Vector<DevPixelSpan, 2> spansToAdd;
Unused << spansToAdd.reserve(2);
spansToAdd.infallibleAppend(urlbarSpan);
if (hasSearchbar) {
spansToAdd.infallibleAppend(searchbarSpan);
}
for (auto& toAdd : spansToAdd) {
for (auto& span : noPlaceholderSpans) {
if (span.start > toAdd.start) {
if (!noPlaceholderSpans.insert(&span, toAdd)) {
return Err(PreXULSkeletonUIError::OOM);
}
break;
}
}
}
for (size_t i = 1; i < noPlaceholderSpans.length(); i++) {
int start = noPlaceholderSpans[i - 1].end + placeholderMargin;
int end = noPlaceholderSpans[i].start - placeholderMargin;
if (start + 2 * placeholderBorderRadius >= end) {
continue;
}
// The placeholder rects should all be y-aligned.
ColorRect placeholderRect = {};
placeholderRect.color = currentTheme.toolbarForegroundColor;
placeholderRect.x = start;
placeholderRect.y = urlbarTextPlaceholder.y;
placeholderRect.width = end - start;
placeholderRect.height = toolbarPlaceholderHeight;
placeholderRect.borderRadius = placeholderBorderRadius;
placeholderRect.flipIfRTL = false;
if (!rects.append(placeholderRect) ||
!sAnimatedRects->append(placeholderRect)) {
return Err(PreXULSkeletonUIError::OOM);
}
}
sTotalChromeHeight = chromeContentDivider.y + chromeContentDivider.height;
if (sTotalChromeHeight > sWindowHeight) {
return Err(PreXULSkeletonUIError::BadWindowDimensions);
}
if (!sAnimatedRects->append(urlbarTextPlaceholder)) {
return Err(PreXULSkeletonUIError::OOM);
}
sPixelBuffer =
(uint32_t*)calloc(sWindowWidth * sTotalChromeHeight, sizeof(uint32_t));
for (auto& rect : *sAnimatedRects) {
if (rtlEnabled && rect.flipIfRTL) {
rect.x = sWindowWidth - rect.x - rect.width;
}
rect.x = std::clamp(rect.x, 0, sWindowWidth);
rect.width = std::clamp(rect.width, 0, sWindowWidth - rect.x);
rect.y = std::clamp(rect.y, 0, sTotalChromeHeight);
rect.height = std::clamp(rect.height, 0, sTotalChromeHeight - rect.y);
}
for (auto& rect : rects) {
if (rtlEnabled && rect.flipIfRTL) {
rect.x = sWindowWidth - rect.x - rect.width;
}
rect.x = std::clamp(rect.x, 0, sWindowWidth);
rect.width = std::clamp(rect.width, 0, sWindowWidth - rect.x);
rect.y = std::clamp(rect.y, 0, sTotalChromeHeight);
rect.height = std::clamp(rect.height, 0, sTotalChromeHeight - rect.y);
RasterizeColorRect(rect);
}
HDC hdc = sGetWindowDC(hWnd);
if (!hdc) {
return Err(PreXULSkeletonUIError::FailedGettingDC);
}
auto cleanupDC = MakeScopeExit([=] { sReleaseDC(hWnd, hdc); });
BITMAPINFO chromeBMI = {};
chromeBMI.bmiHeader.biSize = sizeof(chromeBMI.bmiHeader);
chromeBMI.bmiHeader.biWidth = sWindowWidth;
chromeBMI.bmiHeader.biHeight = -sTotalChromeHeight;
chromeBMI.bmiHeader.biPlanes = 1;
chromeBMI.bmiHeader.biBitCount = 32;
chromeBMI.bmiHeader.biCompression = BI_RGB;
// First, we just paint the chrome area with our pixel buffer
int scanLinesCopied = sStretchDIBits(
hdc, 0, 0, sWindowWidth, sTotalChromeHeight, 0, 0, sWindowWidth,
sTotalChromeHeight, sPixelBuffer, &chromeBMI, DIB_RGB_COLORS, SRCCOPY);
if (scanLinesCopied == 0) {
return Err(PreXULSkeletonUIError::FailedBlitting);
}
// Then, we just fill the rest with FillRect
RECT rect = {0, sTotalChromeHeight, sWindowWidth, sWindowHeight};
bool const fillRectOk =
FillRectWithColor(hdc, &rect, currentTheme.backgroundColor);
if (!fillRectOk) {
return Err(PreXULSkeletonUIError::FailedFillingBottomRect);
}
scopeExit.release();
return Ok();
}
DWORD WINAPI AnimateSkeletonUI(void* aUnused) {
if (!sPixelBuffer || sAnimatedRects->empty()) {
return 0;
}
// See the comments above the InterlockedIncrement calls below here - we
// atomically flip this up and down around sleep so the main thread doesn't
// have to wait for us if we're just sleeping.
if (InterlockedIncrement(&sAnimationControlFlag) != 1) {
return 0;
}
// Sleep for two seconds - startups faster than this don't really benefit
// from an animation, and we don't want to take away cycles from them.
// Startups longer than this, however, are more likely to be blocked on IO,
// and thus animating does not substantially impact startup times for them.
::Sleep(2000);
if (InterlockedDecrement(&sAnimationControlFlag) != 0) {
return 0;
}
// On each of the animated rects (which happen to all be placeholder UI
// rects sharing the same color), we want to animate a gradient moving across
// the screen from left to right. The gradient starts as the rect's color on,
// the left side, changes to the background color of the window by the middle
// of the gradient, and then goes back down to the rect's color. To make this
// faster than interpolating between the two colors for each pixel for each
// frame, we simply create a lookup buffer in which we can look up the color
// for a particular offset into the gradient.
//
// To do this we just interpolate between the two values, and to give the
// gradient a smoother transition between colors, we transform the linear
// blend amount via the cubic smooth step function (SmoothStep3) to produce
// a smooth start and stop for the gradient. We do this for the first half
// of the gradient, and then simply copy that backwards for the second half.
//
// The CSS width of 80 chosen here is effectively is just to match the size
// of the animation provided in the design mockup. We define it in CSS pixels
// simply because the rest of our UI is based off of CSS scalings.
int animationWidth = CSSToDevPixels(80, sCSSToDevPixelScaling);
UniquePtr<uint32_t[]> animationLookup =
MakeUnique<uint32_t[]>(animationWidth);
uint32_t animationColor = sAnimationColor;
NormalizedRGB rgbBlend = UintToRGB(animationColor);
// Build the first half of the lookup table
for (int i = 0; i < animationWidth / 2; ++i) {
uint32_t baseColor = sToolbarForegroundColor;
double blendAmountLinear =
static_cast<double>(i) / (static_cast<double>(animationWidth / 2));
double blendAmount = SmoothStep3(blendAmountLinear);
NormalizedRGB rgbBase = UintToRGB(baseColor);
NormalizedRGB rgb = Lerp(rgbBase, rgbBlend, blendAmount);
animationLookup[i] = RGBToUint(rgb);
}
// Copy the first half of the lookup table into the second half backwards
for (int i = animationWidth / 2; i < animationWidth; ++i) {
int j = animationWidth - 1 - i;
if (j == animationWidth / 2) {
// If animationWidth is odd, we'll be left with one pixel at the center.
// Just color that as the animation color.
animationLookup[i] = animationColor;
} else {
animationLookup[i] = animationLookup[j];
}
}
// The bitmap info remains unchanged throughout the animation - this just
// effectively describes the contents of sPixelBuffer
BITMAPINFO chromeBMI = {};
chromeBMI.bmiHeader.biSize = sizeof(chromeBMI.bmiHeader);
chromeBMI.bmiHeader.biWidth = sWindowWidth;
chromeBMI.bmiHeader.biHeight = -sTotalChromeHeight;
chromeBMI.bmiHeader.biPlanes = 1;
chromeBMI.bmiHeader.biBitCount = 32;
chromeBMI.bmiHeader.biCompression = BI_RGB;
uint32_t animationIteration = 0;
int devPixelsPerFrame =
CSSToDevPixels(kAnimationCSSPixelsPerFrame, sCSSToDevPixelScaling);
int devPixelsExtraWindowSize =
CSSToDevPixels(kAnimationCSSExtraWindowSize, sCSSToDevPixelScaling);
if (::InterlockedCompareExchange(&sAnimationControlFlag, 0, 0)) {
// The window got consumed before we were able to draw anything.
return 0;
}
while (true) {
// The gradient will move across the screen at devPixelsPerFrame at
// 60fps, and then loop back to the beginning. However, we add a buffer of
// devPixelsExtraWindowSize around the edges so it doesn't immediately
// jump back, giving it a more pulsing feel.
int animationMin = ((animationIteration * devPixelsPerFrame) %
(sWindowWidth + devPixelsExtraWindowSize)) -
devPixelsExtraWindowSize / 2;
int animationMax = animationMin + animationWidth;
// The priorAnimationMin is the beginning of the previous frame's animation.
// Since we only want to draw the bits of the image that we updated, we need
// to overwrite the left bit of the animation we drew last frame with the
// default color.
int priorAnimationMin = animationMin - devPixelsPerFrame;
animationMin = std::max(0, animationMin);
priorAnimationMin = std::max(0, priorAnimationMin);
animationMax = std::min((int)sWindowWidth, animationMax);
// The gradient only affects the specific rects that we put into
// sAnimatedRects. So we simply update those rects, and maintain a flag
// to avoid drawing when we don't need to.
bool updatedAnything = false;
for (ColorRect rect : *sAnimatedRects) {
bool hadUpdates =
RasterizeAnimatedRect(rect, animationLookup.get(), priorAnimationMin,
animationMin, animationMax);
updatedAnything = updatedAnything || hadUpdates;
}
if (updatedAnything) {
HDC hdc = sGetWindowDC(sPreXULSkeletonUIWindow);
if (!hdc) {
return 0;
}
sStretchDIBits(hdc, priorAnimationMin, 0,
animationMax - priorAnimationMin, sTotalChromeHeight,
priorAnimationMin, 0, animationMax - priorAnimationMin,
sTotalChromeHeight, sPixelBuffer, &chromeBMI,
DIB_RGB_COLORS, SRCCOPY);
sReleaseDC(sPreXULSkeletonUIWindow, hdc);
}
animationIteration++;
// We coordinate around our sleep here to ensure that the main thread does
// not wait on us if we're sleeping. If we don't get 1 here, it means the
// window has been consumed and we don't need to sleep. If in
// ConsumePreXULSkeletonUIHandle we get a value other than 1 after
// incrementing, it means we're sleeping, and that function can assume that
// we will safely exit after the sleep because of the observed value of
// sAnimationControlFlag.
if (InterlockedIncrement(&sAnimationControlFlag) != 1) {
return 0;
}
// Note: Sleep does not guarantee an exact time interval. If the system is
// busy, for instance, we could easily end up taking several frames longer,
// and really we could be left unscheduled for an arbitrarily long time.
// This is fine, and we don't really care. We could track how much time this
// actually took and jump the animation forward the appropriate amount, but
// its not even clear that that's a better user experience. So we leave this
// as simple as we can.
::Sleep(16);
// Here we bring sAnimationControlFlag back down - again, if we don't get a
// 0 here it means we consumed the skeleton UI window in the mean time, so
// we can simply exit.
if (InterlockedDecrement(&sAnimationControlFlag) != 0) {
return 0;
}
}
}
LRESULT WINAPI PreXULSkeletonUIProc(HWND hWnd, UINT msg, WPARAM wParam,
LPARAM lParam) {
// Exposing a generic oleacc proxy for the skeleton isn't useful and may cause
// screen readers to report spurious information when the skeleton appears.
if (msg == WM_GETOBJECT && sPreXULSkeletonUIWindow) {
return E_FAIL;
}
// NOTE: this block was copied from WinUtils.cpp, and needs to be kept in
// sync.
if (msg == WM_NCCREATE && sEnableNonClientDpiScaling) {
sEnableNonClientDpiScaling(hWnd);
}
// NOTE: this block was paraphrased from the WM_NCCALCSIZE handler in
// nsWindow.cpp, and will need to be kept in sync.
if (msg == WM_NCCALCSIZE) {
RECT* clientRect =
wParam ? &(reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam))->rgrc[0]
: (reinterpret_cast<RECT*>(lParam));
Margin margin = NonClientSizeMargin();
clientRect->top += margin.top;
clientRect->left += margin.left;
clientRect->right -= margin.right;
clientRect->bottom -= margin.bottom;
return 0;
}
return ::DefWindowProcW(hWnd, msg, wParam, lParam);
}
bool IsSystemDarkThemeEnabled() {
DWORD result;
HKEY themeKey;
DWORD dataLen = sizeof(uint32_t);
LPCWSTR keyName =
L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
result = ::RegOpenKeyExW(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &themeKey);
if (result != ERROR_SUCCESS) {
return false;
}
AutoCloseRegKey closeKey(themeKey);
uint32_t lightThemeEnabled;
result = ::RegGetValueW(
themeKey, nullptr, L"AppsUseLightTheme", RRF_RT_REG_DWORD, nullptr,
reinterpret_cast<PBYTE>(&lightThemeEnabled), &dataLen);
if (result != ERROR_SUCCESS) {
return false;
}
return !lightThemeEnabled;
}
ThemeColors GetTheme(ThemeMode themeId) {
ThemeColors theme = {};
switch (themeId) {
case ThemeMode::Dark:
// Dark theme or default theme when in dark mode
// controlled by css variable --toolbar-bgcolor
theme.backgroundColor = 0x2b2a33;
theme.tabColor = 0x42414d;
theme.toolbarForegroundColor = 0x6a6a6d;
theme.tabOutlineColor = 0x1c1b22;
// controlled by css variable --lwt-accent-color
theme.titlebarColor = 0x1c1b22;
// controlled by --toolbar-color in browser.css
theme.chromeContentDividerColor = 0x0c0c0d;
// controlled by css variable --toolbar-field-background-color
theme.urlbarColor = 0x42414d;
theme.urlbarBorderColor = 0x42414d;
theme.animationColor = theme.urlbarColor;
return theme;
case ThemeMode::Light:
case ThemeMode::Default:
default:
// --toolbar-bgcolor in browser.css
theme.backgroundColor = 0xf9f9fb;
theme.tabColor = 0xf9f9fb;
theme.toolbarForegroundColor = 0xdddde1;
theme.tabOutlineColor = 0xdddde1;
// found in browser-aero.css ":root[customtitlebar]:not(:-moz-lwtheme)"
// (set to "hsl(235,33%,19%)")
theme.titlebarColor = 0xf0f0f4;
// --chrome-content-separator-color in browser.css
theme.chromeContentDividerColor = 0xe1e1e2;
// controlled by css variable --toolbar-color
theme.urlbarColor = 0xffffff;
theme.urlbarBorderColor = 0xdddde1;
theme.animationColor = theme.backgroundColor;
return theme;
}
}
Result<HKEY, PreXULSkeletonUIError> OpenPreXULSkeletonUIRegKey() {
HKEY key;
DWORD disposition;
LSTATUS result =
::RegCreateKeyExW(HKEY_CURRENT_USER, kPreXULSkeletonUIKeyPath, 0, nullptr,
0, KEY_ALL_ACCESS, nullptr, &key, &disposition);
if (result != ERROR_SUCCESS) {
return Err(PreXULSkeletonUIError::FailedToOpenRegistryKey);
}
if (disposition == REG_CREATED_NEW_KEY ||
disposition == REG_OPENED_EXISTING_KEY) {
return key;
}
::RegCloseKey(key);
return Err(PreXULSkeletonUIError::FailedToOpenRegistryKey);
}
Result<Ok, PreXULSkeletonUIError> LoadGdi32AndUser32Procedures() {
HMODULE user32Dll = ::LoadLibraryW(L"user32");
HMODULE gdi32Dll = ::LoadLibraryW(L"gdi32");
HMODULE dwmapiDll = ::LoadLibraryW(L"dwmapi.dll");
if (!user32Dll || !gdi32Dll || !dwmapiDll) {
return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs);
}
#define MOZ_LOAD_OR_FAIL(dll_handle, name) \
do { \
s##name = (decltype(&::name))::GetProcAddress(dll_handle, #name); \
if (!s##name) { \
return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); \
} \
} while (0)
auto getThreadDpiAwarenessContext =
(decltype(GetThreadDpiAwarenessContext)*)::GetProcAddress(
user32Dll, "GetThreadDpiAwarenessContext");
auto areDpiAwarenessContextsEqual =
(decltype(AreDpiAwarenessContextsEqual)*)::GetProcAddress(
user32Dll, "AreDpiAwarenessContextsEqual");
if (getThreadDpiAwarenessContext && areDpiAwarenessContextsEqual &&
areDpiAwarenessContextsEqual(getThreadDpiAwarenessContext(),
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) {
// EnableNonClientDpiScaling is first available in Win10 Build 1607, but
// it's optional - we can handle not having it.
Unused << [&]() -> Result<Ok, PreXULSkeletonUIError> {
MOZ_LOAD_OR_FAIL(user32Dll, EnableNonClientDpiScaling);
return Ok{};
}();
}
MOZ_LOAD_OR_FAIL(user32Dll, GetSystemMetricsForDpi);
MOZ_LOAD_OR_FAIL(user32Dll, GetDpiForWindow);
MOZ_LOAD_OR_FAIL(user32Dll, RegisterClassW);
MOZ_LOAD_OR_FAIL(user32Dll, CreateWindowExW);
MOZ_LOAD_OR_FAIL(user32Dll, ShowWindow);
MOZ_LOAD_OR_FAIL(user32Dll, SetWindowPos);
MOZ_LOAD_OR_FAIL(user32Dll, GetWindowDC);
MOZ_LOAD_OR_FAIL(user32Dll, GetWindowRect);
MOZ_LOAD_OR_FAIL(user32Dll, MapWindowPoints);
MOZ_LOAD_OR_FAIL(user32Dll, FillRect);