/* -*- 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 #include #include #include #include #include #include #include #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/UniquePtr.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/Unused.h" #include "mozilla/WindowsDpiAwareness.h" #include "mozilla/WindowsVersion.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(color >> 16 & 0xff) / 255.0; double g = static_cast(color >> 8 & 0xff) / 255.0; double b = static_cast(color >> 0 & 0xff) / 255.0; return NormalizedRGB{r, g, b}; } uint32_t RGBToUint(const NormalizedRGB& rgb) { return (static_cast(rgb.r * 255.0) << 16) | (static_cast(rgb.g * 255.0) << 8) | (static_cast(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); } 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* sAnimatedRects = nullptr; static int sTotalChromeHeight = 0; static volatile LONG sAnimationControlFlag = 0; static bool sMaximized = false; static int sNonClientVerticalMargins = 0; static int sNonClientHorizontalMargins = 0; static uint32_t sDpi = 0; // Color values needed by the animation loop static uint32_t sAnimationColor; static uint32_t sToolbarForegroundColor; static ThemeMode sTheme = ThemeMode::Invalid; typedef BOOL(WINAPI* EnableNonClientDpiScalingProc)(HWND); static EnableNonClientDpiScalingProc sEnableNonClientDpiScaling = NULL; typedef int(WINAPI* GetSystemMetricsForDpiProc)(int, UINT); GetSystemMetricsForDpiProc sGetSystemMetricsForDpi = NULL; typedef UINT(WINAPI* GetDpiForWindowProc)(HWND); GetDpiForWindowProc sGetDpiForWindow = NULL; typedef ATOM(WINAPI* RegisterClassWProc)(const WNDCLASSW*); RegisterClassWProc sRegisterClassW = NULL; typedef HICON(WINAPI* LoadIconWProc)(HINSTANCE, LPCWSTR); LoadIconWProc sLoadIconW = NULL; typedef HICON(WINAPI* LoadCursorWProc)(HINSTANCE, LPCWSTR); LoadCursorWProc sLoadCursorW = NULL; typedef HWND(WINAPI* CreateWindowExWProc)(DWORD, LPCWSTR, LPCWSTR, DWORD, int, int, int, int, HWND, HMENU, HINSTANCE, LPVOID); CreateWindowExWProc sCreateWindowExW = NULL; typedef BOOL(WINAPI* ShowWindowProc)(HWND, int); ShowWindowProc sShowWindow = NULL; typedef BOOL(WINAPI* SetWindowPosProc)(HWND, HWND, int, int, int, int, UINT); SetWindowPosProc sSetWindowPos = NULL; typedef HDC(WINAPI* GetWindowDCProc)(HWND); GetWindowDCProc sGetWindowDC = NULL; typedef int(WINAPI* FillRectProc)(HDC, const RECT*, HBRUSH); FillRectProc sFillRect = NULL; typedef BOOL(WINAPI* DeleteObjectProc)(HGDIOBJ); DeleteObjectProc sDeleteObject = NULL; typedef int(WINAPI* ReleaseDCProc)(HWND, HDC); ReleaseDCProc sReleaseDC = NULL; typedef HMONITOR(WINAPI* MonitorFromWindowProc)(HWND, DWORD); MonitorFromWindowProc sMonitorFromWindow = NULL; typedef BOOL(WINAPI* GetMonitorInfoWProc)(HMONITOR, LPMONITORINFO); GetMonitorInfoWProc sGetMonitorInfoW = NULL; typedef LONG_PTR(WINAPI* SetWindowLongPtrWProc)(HWND, int, LONG_PTR); SetWindowLongPtrWProc sSetWindowLongPtrW = NULL; typedef int(WINAPI* StretchDIBitsProc)(HDC, int, int, int, int, int, int, int, int, const VOID*, const BITMAPINFO*, UINT, DWORD); StretchDIBitsProc sStretchDIBits = NULL; typedef HBRUSH(WINAPI* CreateSolidBrushProc)(COLORREF); CreateSolidBrushProc sCreateSolidBrush = NULL; static int sWindowWidth; static int sWindowHeight; static double sCSSToDevPixelScaling; static Maybe 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, PreXULSkeletonUIError> GetBinaryPath() { DWORD bufLen = MAX_PATH; UniquePtr buf; while (true) { buf = MakeUnique(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 // https://searchfox.org/mozilla-central/rev/71621bfa47a371f2b1ccfd33c704913124afb933/toolkit/components/remote/nsRemoteService.cpp#56 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 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 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 installHash = MakeUnique(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 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 // https://searchfox.org/mozilla-central/rev/a5d9abfda1e26b1207db9549549ab0bdd73f735d/gfx/wr/webrender/res/shared.glsl#141-187 // 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::epsilon()) return 1.0; if (dist >= 0.5 - std::numeric_limits::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::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 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; } Result DrawSkeletonUI( HWND hWnd, CSSPixelSpan urlbarCSSSpan, CSSPixelSpan searchbarCSSSpan, Vector& springs, const ThemeColors& currentTheme, const EnumSet& 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 bookmarksToolbarShown = flags.contains(SkeletonUIFlag::BookmarksToolbarShown); bool rtlEnabled = flags.contains(SkeletonUIFlag::RtlEnabled); int chromeHorMargin = CSSToDevPixels(2, sCSSToDevPixelScaling); int verticalOffset = sMaximized ? sNonClientVerticalMargins : 0; int horizontalOffset = sNonClientHorizontalMargins - (sMaximized ? 0 : chromeHorMargin); // found in tabs.inc.css, "--tab-min-height" + 2 * "--tab-block-margin" int tabBarHeight = 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 rects; ColorRect menubar = {}; menubar.color = currentTheme.tabBarColor; 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.tabBarColor; 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); } // 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); } // The toolbar background ColorRect toolbar = {}; toolbar.color = 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 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 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(tabTextPlaceholder) || !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}; HBRUSH brush = sCreateSolidBrush(RGB((currentTheme.backgroundColor & 0xff0000) >> 16, (currentTheme.backgroundColor & 0x00ff00) >> 8, (currentTheme.backgroundColor & 0x0000ff) >> 0)); int fillRectResult = sFillRect(hdc, &rect, brush); sDeleteObject(brush); if (fillRectResult == 0) { 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 animationLookup = MakeUnique(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(i) / (static_cast(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(lParam))->rgrc[0] : (reinterpret_cast(lParam)); // These match the margins set in browser-tabsintitlebar.js with // default prefs on Windows. Bug 1673092 tracks lining this up with // that more correctly instead of hard-coding it. int horizontalOffset = sNonClientHorizontalMargins - (sMaximized ? 0 : CSSToDevPixels(2, sCSSToDevPixelScaling)); int verticalOffset = sNonClientHorizontalMargins - (sMaximized ? 0 : CSSToDevPixels(2, sCSSToDevPixelScaling)); clientRect->top = clientRect->top; clientRect->left += horizontalOffset; clientRect->right -= horizontalOffset; clientRect->bottom -= verticalOffset; 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(&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.tabBarColor = 0x1c1b22; // controlled by --toolbar-non-lwt-textcolor 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-non-lwt-bgcolor in browser.css theme.backgroundColor = 0xf9f9fb; theme.tabColor = 0xf9f9fb; theme.toolbarForegroundColor = 0xdddde1; theme.tabOutlineColor = 0xdddde1; // found in browser-aero.css ":root[tabsintitlebar]:not(:-moz-lwtheme)" // (set to "hsl(235,33%,19%)") theme.tabBarColor = 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 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 LoadGdi32AndUser32Procedures() { HMODULE user32Dll = ::LoadLibraryW(L"user32"); HMODULE gdi32Dll = ::LoadLibraryW(L"gdi32"); if (!user32Dll || !gdi32Dll) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } 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 optional - we can handle not having it. sEnableNonClientDpiScaling = (EnableNonClientDpiScalingProc)::GetProcAddress( user32Dll, "EnableNonClientDpiScaling"); } sGetSystemMetricsForDpi = (GetSystemMetricsForDpiProc)::GetProcAddress( user32Dll, "GetSystemMetricsForDpi"); if (!sGetSystemMetricsForDpi) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sGetDpiForWindow = (GetDpiForWindowProc)::GetProcAddress(user32Dll, "GetDpiForWindow"); if (!sGetDpiForWindow) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sRegisterClassW = (RegisterClassWProc)::GetProcAddress(user32Dll, "RegisterClassW"); if (!sRegisterClassW) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sCreateWindowExW = (CreateWindowExWProc)::GetProcAddress(user32Dll, "CreateWindowExW"); if (!sCreateWindowExW) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sShowWindow = (ShowWindowProc)::GetProcAddress(user32Dll, "ShowWindow"); if (!sShowWindow) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sSetWindowPos = (SetWindowPosProc)::GetProcAddress(user32Dll, "SetWindowPos"); if (!sSetWindowPos) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sGetWindowDC = (GetWindowDCProc)::GetProcAddress(user32Dll, "GetWindowDC"); if (!sGetWindowDC) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sFillRect = (FillRectProc)::GetProcAddress(user32Dll, "FillRect"); if (!sFillRect) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sReleaseDC = (ReleaseDCProc)::GetProcAddress(user32Dll, "ReleaseDC"); if (!sReleaseDC) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sLoadIconW = (LoadIconWProc)::GetProcAddress(user32Dll, "LoadIconW"); if (!sLoadIconW) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sLoadCursorW = (LoadCursorWProc)::GetProcAddress(user32Dll, "LoadCursorW"); if (!sLoadCursorW) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sMonitorFromWindow = (MonitorFromWindowProc)::GetProcAddress(user32Dll, "MonitorFromWindow"); if (!sMonitorFromWindow) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sGetMonitorInfoW = (GetMonitorInfoWProc)::GetProcAddress(user32Dll, "GetMonitorInfoW"); if (!sGetMonitorInfoW) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sSetWindowLongPtrW = (SetWindowLongPtrWProc)::GetProcAddress(user32Dll, "SetWindowLongPtrW"); if (!sSetWindowLongPtrW) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sStretchDIBits = (StretchDIBitsProc)::GetProcAddress(gdi32Dll, "StretchDIBits"); if (!sStretchDIBits) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sCreateSolidBrush = (CreateSolidBrushProc)::GetProcAddress(gdi32Dll, "CreateSolidBrush"); if (!sCreateSolidBrush) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } sDeleteObject = (DeleteObjectProc)::GetProcAddress(gdi32Dll, "DeleteObject"); if (!sDeleteObject) { return Err(PreXULSkeletonUIError::FailedLoadingDynamicProcs); } return Ok(); } // Strips "--", "-", and "/" from the front of the arg if one of those exists, // returning `arg + 2`, `arg + 1`, and `arg + 1` respectively. If none of these // prefixes are found, the argument is not a flag, and nullptr is returned. const char* NormalizeFlag(const char* arg) { if (strstr(arg, "--") == arg) { return arg + 2; } if (arg[0] == '-') { return arg + 1; } if (arg[0] == '/') { return arg + 1; } return nullptr; } static bool EnvHasValue(const char* name) { const char* val = getenv(name); return (val && *val); } // Ensures that we only see arguments in the command line which are acceptable. // This is based on manual inspection of the list of arguments listed in the MDN // page for Gecko/Firefox commandline options: // https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options // Broadly speaking, we want to reject any argument which causes us to show // something other than the default window at its normal size. Here is a non- // exhaustive list of command line options we want to *exclude*: // // -ProfileManager : This will display the profile manager window, which does // not match the skeleton UI at all. // // -CreateProfile : This will display a firefox window with the default // screen position and size, and not the position and size // which we have recorded in the registry. // // -P : This could cause us to display firefox with a position // and size of a different profile than that in which we // were previously running. // // -width, -height : This will cause the width and height values in the // registry to be incorrect. // // -kiosk : See above. // // -headless : This one should be rather obvious. // // -migration : This will start with the import wizard, which of course // does not match the skeleton UI. // // -private-window : This is tricky, but the colors of the main content area // make this not feel great with the white content of the // default skeleton UI. // // NOTE: we generally want to skew towards erroneous rejections of the command // line rather than erroneous approvals. The consequence of a bad rejection // is that we don't show the skeleton UI, which is business as usual. The // consequence of a bad approval is that we show it when we're not supposed to, // which is visually jarring and can also be unpredictable - there's no // guarantee that the code which handles the non-default window is set up to // properly handle the transition from the skeleton UI window. static Result ValidateCmdlineArguments( int argc, char** argv, bool* explicitProfile) { const char* approvedArgumentsArray[] = { // These won't cause the browser to be visualy different in any way "new-instance", "no-remote", "browser", "foreground", "setDefaultBrowser", "attach-console", "wait-for-browser", "osint", // These will cause the chrome to be a bit different or extra windows to // be created, but overall the skeleton UI should still be broadly // correct enough. "new-tab", "new-window", // To the extent possible, we want to ensure that existing tests cover // the skeleton UI, so we need to allow marionette "marionette", // These will cause the content area to appear different, but won't // meaningfully affect the chrome "preferences", "search", "url", #ifndef MOZILLA_OFFICIAL // On local builds, we want to allow -profile, because it's how `mach run` // operates, and excluding that would create an unnecessary blind spot for // Firefox devs. "profile" #endif // There are other arguments which are likely okay. However, they are // not included here because this list is not intended to be // exhaustive - it only intends to green-light some somewhat commonly // used arguments. We want to err on the side of an unnecessary // rejection of the command line. }; int approvedArgumentsArraySize = sizeof(approvedArgumentsArray) / sizeof(approvedArgumentsArray[0]); Vector approvedArguments; if (!approvedArguments.reserve(approvedArgumentsArraySize)) { return Err(PreXULSkeletonUIError::OOM); } for (int i = 0; i < approvedArgumentsArraySize; ++i) { approvedArguments.infallibleAppend(approvedArgumentsArray[i]); } #ifdef MOZILLA_OFFICIAL int profileArgIndex = -1; // If we're running mochitests or direct marionette tests, those specify a // temporary profile, and we want to ensure that we get the added coverage // from those. for (int i = 1; i < argc; ++i) { const char* flag = NormalizeFlag(argv[i]); if (flag && !strcmp(flag, "marionette")) { if (!approvedArguments.append("profile")) { return Err(PreXULSkeletonUIError::OOM); } profileArgIndex = approvedArguments.length() - 1; break; } } #else int profileArgIndex = approvedArguments.length() - 1; #endif for (int i = 1; i < argc; ++i) { const char* flag = NormalizeFlag(argv[i]); if (!flag) { // If this is not a flag, then we interpret it as a URL, similar to // BrowserContentHandler.sys.mjs. Some command line options take // additional arguments, which may or may not be URLs. We don't need to // know this, because we don't need to parse them out; we just rely on the // assumption that if arg X is actually a parameter for the preceding // arg Y, then X must not look like a flag (starting with "--", "-", // or "/"). // // The most important thing here is the assumption that if something is // going to meaningfully alter the appearance of the window itself, it // must be a flag. continue; } bool approved = false; for (const char* approvedArg : approvedArguments) { // We do a case-insensitive compare here with _stricmp. Even though some // of these arguments are *not* read as case-insensitive, others *are*. // Similar to the flag logic above, we don't really care about this // distinction, because we don't need to parse the arguments - we just // rely on the assumption that none of the listed flags in our // approvedArguments are overloaded in such a way that a different // casing would visually alter the firefox window. if (!_stricmp(flag, approvedArg)) { approved = true; if (i == profileArgIndex) { *explicitProfile = true; } break; } } if (!approved) { return Err(PreXULSkeletonUIError::Cmdline); } } return Ok(); } static Result ValidateEnvVars() { if (EnvHasValue("MOZ_SAFE_MODE_RESTART") || EnvHasValue("MOZ_APP_SILENT_START") || EnvHasValue("MOZ_RESET_PROFILE_RESTART") || EnvHasValue("MOZ_HEADLESS") || (EnvHasValue("XRE_PROFILE_PATH") && !EnvHasValue("MOZ_SKELETON_UI_RESTARTING"))) { return Err(PreXULSkeletonUIError::EnvVars); } return Ok(); } static bool VerifyWindowDimensions(uint32_t windowWidth, uint32_t windowHeight) { return windowWidth <= kMaxWindowWidth && windowHeight <= kMaxWindowHeight; } static Result, PreXULSkeletonUIError> ReadRegCSSPixelSpans( HKEY regKey, const std::wstring& valueName) { DWORD dataLen = 0; LSTATUS result = ::RegQueryValueExW(regKey, valueName.c_str(), nullptr, nullptr, nullptr, &dataLen); if (result != ERROR_SUCCESS) { return Err(PreXULSkeletonUIError::RegistryError); } if (dataLen % (2 * sizeof(double)) != 0) { return Err(PreXULSkeletonUIError::CorruptData); } auto buffer = MakeUniqueFallible(dataLen); if (!buffer) { return Err(PreXULSkeletonUIError::OOM); } result = ::RegGetValueW(regKey, nullptr, valueName.c_str(), RRF_RT_REG_BINARY, nullptr, reinterpret_cast(buffer.get()), &dataLen); if (result != ERROR_SUCCESS) { return Err(PreXULSkeletonUIError::RegistryError); } Vector resultVector; double* asDoubles = reinterpret_cast(buffer.get()); for (size_t i = 0; i < dataLen / (2 * sizeof(double)); i++) { CSSPixelSpan span = {}; span.start = *(asDoubles++); span.end = *(asDoubles++); if (!resultVector.append(span)) { return Err(PreXULSkeletonUIError::OOM); } } return resultVector; } static Result ReadRegDouble( HKEY regKey, const std::wstring& valueName) { double value = 0; DWORD dataLen = sizeof(double); LSTATUS result = ::RegGetValueW(regKey, nullptr, valueName.c_str(), RRF_RT_REG_BINARY, nullptr, reinterpret_cast(&value), &dataLen); if (result != ERROR_SUCCESS || dataLen != sizeof(double)) { return Err(PreXULSkeletonUIError::RegistryError); } return value; } static Result ReadRegUint( HKEY regKey, const std::wstring& valueName) { DWORD value = 0; DWORD dataLen = sizeof(uint32_t); LSTATUS result = ::RegGetValueW(regKey, nullptr, valueName.c_str(), RRF_RT_REG_DWORD, nullptr, reinterpret_cast(&value), &dataLen); if (result != ERROR_SUCCESS) { return Err(PreXULSkeletonUIError::RegistryError); } return value; } static Result ReadRegBool( HKEY regKey, const std::wstring& valueName) { uint32_t value; MOZ_TRY_VAR(value, ReadRegUint(regKey, valueName)); return !!value; } static Result WriteRegCSSPixelSpans( HKEY regKey, const std::wstring& valueName, const CSSPixelSpan* spans, int spansLength) { // No guarantee on the packing of CSSPixelSpan. We could #pragma it, but it's // also trivial to just copy them into a buffer of doubles. auto doubles = MakeUnique(spansLength * 2); for (int i = 0; i < spansLength; ++i) { doubles[i * 2] = spans[i].start; doubles[i * 2 + 1] = spans[i].end; } LSTATUS result = ::RegSetValueExW(regKey, valueName.c_str(), 0, REG_BINARY, reinterpret_cast(doubles.get()), spansLength * sizeof(double) * 2); if (result != ERROR_SUCCESS) { return Err(PreXULSkeletonUIError::RegistryError); } return Ok(); } static Result WriteRegDouble( HKEY regKey, const std::wstring& valueName, double value) { LSTATUS result = ::RegSetValueExW(regKey, valueName.c_str(), 0, REG_BINARY, reinterpret_cast(&value), sizeof(value)); if (result != ERROR_SUCCESS) { return Err(PreXULSkeletonUIError::RegistryError); } return Ok(); } static Result WriteRegUint( HKEY regKey, const std::wstring& valueName, uint32_t value) { LSTATUS result = ::RegSetValueExW(regKey, valueName.c_str(), 0, REG_DWORD, reinterpret_cast(&value), sizeof(value)); if (result != ERROR_SUCCESS) { return Err(PreXULSkeletonUIError::RegistryError); } return Ok(); } static Result WriteRegBool( HKEY regKey, const std::wstring& valueName, bool value) { return WriteRegUint(regKey, valueName, value ? 1 : 0); } static Result CreateAndStorePreXULSkeletonUIImpl( HINSTANCE hInstance, int argc, char** argv) { // Initializing COM below may load modules via SetWindowHookEx, some of // which may modify the executable's IAT for ntdll.dll. If that happens, // this browser process fails to launch sandbox processes because we cannot // copy a modified IAT into a remote process (See SandboxBroker::LaunchApp). // To prevent that, we cache the intact IAT before COM initialization. // If EAF+ is enabled, CacheNtDllThunk() causes a crash, but EAF+ will // also prevent an injected module from parsing the PE headers and modifying // the IAT. Therefore, we can skip CacheNtDllThunk(). if (!mozilla::IsEafPlusEnabled()) { CacheNtDllThunk(); } // NOTE: it's important that we initialize sProcessRuntime before showing a // window. Historically we ran into issues where showing the window would // cause an accessibility win event to fire, which could cause in-process // system or third party components to initialize COM and prevent us from // initializing it with important settings we need. // Some COM settings are global to the process and must be set before any non- // trivial COM is run in the application. Since these settings may affect // stability, we should instantiate COM ASAP so that we can ensure that these // global settings are configured before anything can interfere. sProcessRuntime = new mscom::ProcessRuntime( mscom::ProcessRuntime::ProcessCategory::GeckoBrowserParent); const TimeStamp skeletonStart = TimeStamp::Now(); if (!IsWin10OrLater()) { return Err(PreXULSkeletonUIError::Ineligible); } HKEY regKey; MOZ_TRY_VAR(regKey, OpenPreXULSkeletonUIRegKey()); AutoCloseRegKey closeKey(regKey); UniquePtr binPath; MOZ_TRY_VAR(binPath, GetBinaryPath()); std::wstring regProgressName = GetRegValueName(binPath.get(), sProgressSuffix); auto progressResult = ReadRegUint(regKey, regProgressName); if (!progressResult.isErr() && progressResult.unwrap() != static_cast(PreXULSkeletonUIProgress::Completed)) { return Err(PreXULSkeletonUIError::CrashedOnce); } MOZ_TRY( WriteRegUint(regKey, regProgressName, static_cast(PreXULSkeletonUIProgress::Started))); auto writeCompletion = MakeScopeExit([&] { Unused << WriteRegUint( regKey, regProgressName, static_cast(PreXULSkeletonUIProgress::Completed)); }); MOZ_TRY(GetSkeletonUILock()); bool explicitProfile = false; MOZ_TRY(ValidateCmdlineArguments(argc, argv, &explicitProfile)); MOZ_TRY(ValidateEnvVars()); auto enabledResult = ReadRegBool(regKey, GetRegValueName(binPath.get(), sEnabledRegSuffix)); if (enabledResult.isErr()) { return Err(PreXULSkeletonUIError::EnabledKeyDoesNotExist); } if (!enabledResult.unwrap()) { return Err(PreXULSkeletonUIError::Disabled); } sPreXULSkeletonUIEnabled = true; MOZ_ASSERT(!sAnimatedRects); sAnimatedRects = new Vector(); MOZ_TRY(LoadGdi32AndUser32Procedures()); if (!explicitProfile) { MOZ_TRY(CheckForStartWithLastProfile()); } WNDCLASSW wc; wc.style = CS_DBLCLKS; wc.lpfnWndProc = PreXULSkeletonUIProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = sLoadIconW(::GetModuleHandleW(nullptr), gStockApplicationIcon); wc.hCursor = sLoadCursorW(hInstance, gIDCWait); wc.hbrBackground = nullptr; wc.lpszMenuName = nullptr; // TODO: just ensure we disable this if we've overridden the window class wc.lpszClassName = L"MozillaWindowClass"; if (!sRegisterClassW(&wc)) { return Err(PreXULSkeletonUIError::FailedRegisteringWindowClass); } uint32_t screenX; MOZ_TRY_VAR(screenX, ReadRegUint(regKey, GetRegValueName(binPath.get(), sScreenXRegSuffix))); uint32_t screenY; MOZ_TRY_VAR(screenY, ReadRegUint(regKey, GetRegValueName(binPath.get(), sScreenYRegSuffix))); uint32_t windowWidth; MOZ_TRY_VAR( windowWidth, ReadRegUint(regKey, GetRegValueName(binPath.get(), sWidthRegSuffix))); uint32_t windowHeight; MOZ_TRY_VAR( windowHeight, ReadRegUint(regKey, GetRegValueName(binPath.get(), sHeightRegSuffix))); MOZ_TRY_VAR( sMaximized, ReadRegBool(regKey, GetRegValueName(binPath.get(), sMaximizedRegSuffix))); MOZ_TRY_VAR( sCSSToDevPixelScaling, ReadRegDouble(regKey, GetRegValueName(binPath.get(), sCssToDevPixelScalingRegSuffix))); Vector urlbar; MOZ_TRY_VAR(urlbar, ReadRegCSSPixelSpans( regKey, GetRegValueName(binPath.get(), sUrlbarCSSRegSuffix))); Vector searchbar; MOZ_TRY_VAR(searchbar, ReadRegCSSPixelSpans( regKey, GetRegValueName(binPath.get(), sSearchbarRegSuffix))); Vector springs; MOZ_TRY_VAR(springs, ReadRegCSSPixelSpans( regKey, GetRegValueName(binPath.get(), sSpringsCSSRegSuffix))); if (urlbar.empty() || searchbar.empty()) { return Err(PreXULSkeletonUIError::CorruptData); } EnumSet flags; uint32_t flagsUint; MOZ_TRY_VAR(flagsUint, ReadRegUint(regKey, GetRegValueName(binPath.get(), sFlagsRegSuffix))); flags.deserialize(flagsUint); if (flags.contains(SkeletonUIFlag::TouchDensity) || flags.contains(SkeletonUIFlag::CompactDensity)) { return Err(PreXULSkeletonUIError::BadUIDensity); } uint32_t theme; MOZ_TRY_VAR(theme, ReadRegUint(regKey, GetRegValueName(binPath.get(), sThemeRegSuffix))); ThemeMode themeMode = static_cast(theme); if (themeMode == ThemeMode::Default) { if (IsSystemDarkThemeEnabled() == true) { themeMode = ThemeMode::Dark; } } ThemeColors currentTheme = GetTheme(themeMode); if (!VerifyWindowDimensions(windowWidth, windowHeight)) { return Err(PreXULSkeletonUIError::BadWindowDimensions); } int showCmd = SW_SHOWNORMAL; DWORD windowStyle = kPreXULSkeletonUIWindowStyle; if (sMaximized) { showCmd = SW_SHOWMAXIMIZED; windowStyle |= WS_MAXIMIZE; } sPreXULSkeletonUIWindow = sCreateWindowExW(kPreXULSkeletonUIWindowStyleEx, L"MozillaWindowClass", L"", windowStyle, screenX, screenY, windowWidth, windowHeight, nullptr, nullptr, hInstance, nullptr); if (!sPreXULSkeletonUIWindow) { return Err(PreXULSkeletonUIError::CreateWindowFailed); } sShowWindow(sPreXULSkeletonUIWindow, showCmd); sDpi = sGetDpiForWindow(sPreXULSkeletonUIWindow); sNonClientHorizontalMargins = sGetSystemMetricsForDpi(SM_CXFRAME, sDpi) + sGetSystemMetricsForDpi(SM_CXPADDEDBORDER, sDpi); sNonClientVerticalMargins = sGetSystemMetricsForDpi(SM_CYFRAME, sDpi) + sGetSystemMetricsForDpi(SM_CXPADDEDBORDER, sDpi); if (sMaximized) { HMONITOR monitor = sMonitorFromWindow(sPreXULSkeletonUIWindow, MONITOR_DEFAULTTONULL); if (!monitor) { // NOTE: we specifically don't clean up the window here. If we're unable // to finish setting up the window how we want it, we still need to keep // it around and consume it with the first real toplevel window we // create, to avoid flickering. return Err(PreXULSkeletonUIError::FailedGettingMonitorInfo); } MONITORINFO mi = {sizeof(MONITORINFO)}; if (!sGetMonitorInfoW(monitor, &mi)) { return Err(PreXULSkeletonUIError::FailedGettingMonitorInfo); } sWindowWidth = mi.rcWork.right - mi.rcWork.left + sNonClientHorizontalMargins * 2; sWindowHeight = mi.rcWork.bottom - mi.rcWork.top + sNonClientVerticalMargins * 2; } else { sWindowWidth = static_cast(windowWidth); sWindowHeight = static_cast(windowHeight); } sSetWindowPos(sPreXULSkeletonUIWindow, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOZORDER); MOZ_TRY(DrawSkeletonUI(sPreXULSkeletonUIWindow, urlbar[0], searchbar[0], springs, currentTheme, flags)); if (sAnimatedRects) { sPreXULSKeletonUIAnimationThread = ::CreateThread( nullptr, 256 * 1024, AnimateSkeletonUI, nullptr, 0, nullptr); } BASE_PROFILER_MARKER_UNTYPED( "CreatePreXULSkeletonUI", OTHER, MarkerTiming::IntervalUntilNowFrom(skeletonStart)); return Ok(); } void CreateAndStorePreXULSkeletonUI(HINSTANCE hInstance, int argc, char** argv) { auto result = CreateAndStorePreXULSkeletonUIImpl(hInstance, argc, argv); if (result.isErr()) { sErrorReason.emplace(result.unwrapErr()); } } void CleanupProcessRuntime() { delete sProcessRuntime; sProcessRuntime = nullptr; } bool WasPreXULSkeletonUIMaximized() { return sMaximized; } bool GetPreXULSkeletonUIWasShown() { return sPreXULSkeletonUIShown || !!sPreXULSkeletonUIWindow; } HWND ConsumePreXULSkeletonUIHandle() { // NOTE: we need to make sure that everything that runs here is a no-op if // it failed to be set, which is a possibility. If anything fails to be set // we don't want to clean everything up right away, because if we have a // blank window up, we want that to stick around and get consumed by nsWindow // as normal, otherwise the window will flicker in and out, which we imagine // is unpleasant. // If we don't get 1 here, it means the thread is actually just sleeping, so // we don't need to worry about giving out ownership of the window, because // the thread will simply exit after its sleep. However, if it is 1, we need // to wait for the thread to exit to be safe, as it could be doing anything. if (InterlockedIncrement(&sAnimationControlFlag) == 1) { ::WaitForSingleObject(sPreXULSKeletonUIAnimationThread, INFINITE); } ::CloseHandle(sPreXULSKeletonUIAnimationThread); sPreXULSKeletonUIAnimationThread = nullptr; HWND result = sPreXULSkeletonUIWindow; sPreXULSkeletonUIWindow = nullptr; free(sPixelBuffer); sPixelBuffer = nullptr; delete sAnimatedRects; sAnimatedRects = nullptr; return result; } Maybe GetPreXULSkeletonUIErrorReason() { return sErrorReason; } Result PersistPreXULSkeletonUIValues( const SkeletonUISettings& settings) { if (!sPreXULSkeletonUIEnabled) { return Err(PreXULSkeletonUIError::Disabled); } HKEY regKey; MOZ_TRY_VAR(regKey, OpenPreXULSkeletonUIRegKey()); AutoCloseRegKey closeKey(regKey); UniquePtr binPath; MOZ_TRY_VAR(binPath, GetBinaryPath()); MOZ_TRY(WriteRegUint(regKey, GetRegValueName(binPath.get(), sScreenXRegSuffix), settings.screenX)); MOZ_TRY(WriteRegUint(regKey, GetRegValueName(binPath.get(), sScreenYRegSuffix), settings.screenY)); MOZ_TRY(WriteRegUint(regKey, GetRegValueName(binPath.get(), sWidthRegSuffix), settings.width)); MOZ_TRY(WriteRegUint(regKey, GetRegValueName(binPath.get(), sHeightRegSuffix), settings.height)); MOZ_TRY(WriteRegBool(regKey, GetRegValueName(binPath.get(), sMaximizedRegSuffix), settings.maximized)); EnumSet flags; if (settings.menubarShown) { flags += SkeletonUIFlag::MenubarShown; } if (settings.bookmarksToolbarShown) { flags += SkeletonUIFlag::BookmarksToolbarShown; } if (settings.rtlEnabled) { flags += SkeletonUIFlag::RtlEnabled; } if (settings.uiDensity == SkeletonUIDensity::Touch) { flags += SkeletonUIFlag::TouchDensity; } if (settings.uiDensity == SkeletonUIDensity::Compact) { flags += SkeletonUIFlag::CompactDensity; } uint32_t flagsUint = flags.serialize(); MOZ_TRY(WriteRegUint(regKey, GetRegValueName(binPath.get(), sFlagsRegSuffix), flagsUint)); MOZ_TRY(WriteRegDouble( regKey, GetRegValueName(binPath.get(), sCssToDevPixelScalingRegSuffix), settings.cssToDevPixelScaling)); MOZ_TRY(WriteRegCSSPixelSpans( regKey, GetRegValueName(binPath.get(), sUrlbarCSSRegSuffix), &settings.urlbarSpan, 1)); MOZ_TRY(WriteRegCSSPixelSpans( regKey, GetRegValueName(binPath.get(), sSearchbarRegSuffix), &settings.searchbarSpan, 1)); MOZ_TRY(WriteRegCSSPixelSpans( regKey, GetRegValueName(binPath.get(), sSpringsCSSRegSuffix), settings.springs.begin(), settings.springs.length())); return Ok(); } MFBT_API bool GetPreXULSkeletonUIEnabled() { return sPreXULSkeletonUIEnabled; } MFBT_API Result SetPreXULSkeletonUIEnabledIfAllowed( bool value) { // If the pre-XUL skeleton UI was disallowed for some reason, we just want to // ignore changes to the registry. An example of how things could be bad if // we didn't: someone running firefox with the -profile argument could // turn the skeleton UI on or off for the default profile. Turning it off // maybe isn't so bad (though it's likely still incorrect), but turning it // on could be bad if the user had specifically disabled it for a profile for // some reason. Ultimately there's no correct decision here, and the // messiness of this is just a consequence of sharing the registry values // across profiles. However, whatever ill effects we observe should be // correct themselves after one session. if (PreXULSkeletonUIDisallowed()) { return Err(PreXULSkeletonUIError::Disabled); } HKEY regKey; MOZ_TRY_VAR(regKey, OpenPreXULSkeletonUIRegKey()); AutoCloseRegKey closeKey(regKey); UniquePtr binPath; MOZ_TRY_VAR(binPath, GetBinaryPath()); MOZ_TRY(WriteRegBool( regKey, GetRegValueName(binPath.get(), sEnabledRegSuffix), value)); if (!sPreXULSkeletonUIEnabled && value) { // We specifically don't care if we fail to get this lock. We just want to // do our best effort to lock it so that future instances don't create // skeleton UIs while we're still running, since they will immediately exit // and tell us to open a new window. Unused << GetSkeletonUILock(); } sPreXULSkeletonUIEnabled = value; return Ok(); } MFBT_API Result SetPreXULSkeletonUIThemeId( ThemeMode theme) { if (theme == sTheme) { return Ok(); } sTheme = theme; // If we fail below, invalidate sTheme auto invalidateTheme = MakeScopeExit([] { sTheme = ThemeMode::Invalid; }); HKEY regKey; MOZ_TRY_VAR(regKey, OpenPreXULSkeletonUIRegKey()); AutoCloseRegKey closeKey(regKey); UniquePtr binPath; MOZ_TRY_VAR(binPath, GetBinaryPath()); MOZ_TRY(WriteRegUint(regKey, GetRegValueName(binPath.get(), sThemeRegSuffix), static_cast(theme))); invalidateTheme.release(); return Ok(); } MFBT_API void PollPreXULSkeletonUIEvents() { if (sPreXULSkeletonUIEnabled && sPreXULSkeletonUIWindow) { MSG outMsg = {}; PeekMessageW(&outMsg, sPreXULSkeletonUIWindow, 0, 0, 0); } } Result NotePreXULSkeletonUIRestarting() { if (!sPreXULSkeletonUIEnabled) { return Err(PreXULSkeletonUIError::Disabled); } ::SetEnvironmentVariableW(L"MOZ_SKELETON_UI_RESTARTING", L"1"); // We assume that we are going to exit the application very shortly after // this. It should thus be fine to release this lock, and we'll need to, // since during a restart we launch the new instance before closing this // one. if (sPreXULSKeletonUILockFile != INVALID_HANDLE_VALUE) { ::CloseHandle(sPreXULSKeletonUILockFile); } return Ok(); } } // namespace mozilla