皮肤在 candidate_window.backdrop 写 acrylic 或 mica 时,候选卡片圆角范围内垫一层系统材质: acrylic 模糊后面的窗口,mica 模糊桌面壁纸(Windows 11,更早的系统按 acrylic)。材质按 Fluent 配方叠「模糊背景 → 亮度层 → 色调」,色调取 surface,深浅主题各自用 backdrop_opacity / backdrop_luminosity 调色调与亮度层的不透明度。 msimeui 的 DeviceResources 新增 backdrop composition:经 Windows.UI.Composition 把 D2D 画进 CompositionDrawingSurface,与材质的形状在同一次提交里更新,卡片伸缩时边缘不错位; 交换链 Present 与合成提交分属两条路径,实测会错开。没写 backdrop 的皮肤仍走交换链。 有材质时卡片阴影只画在卡片外,ShadowCache 改按 D2D 设备判断缓存是否可用。 系统关闭透明效果或建不起 Windows.UI.Composition 时退回交换链,surface 按不透明画。 附 acrylic-demo、mica-demo 两个样例皮肤。
276 lines
13 KiB
C++
276 lines
13 KiB
C++
#pragma once
|
|
|
|
#include <d2d1.h>
|
|
#include <d2d1_1.h>
|
|
#include <d3d11.h>
|
|
#include <dcomp.h>
|
|
#include <dxgi1_2.h>
|
|
#include <dwrite.h>
|
|
#include <wincodec.h>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <wrl/client.h>
|
|
|
|
#include "AnimatedImage.h"
|
|
#include "Types.h"
|
|
|
|
namespace msimeui
|
|
{
|
|
// What fills the backdrop rectangle of DeviceResources::SetBackdrop, after the recipe of the
|
|
// Fluent acrylic and Mica materials: a blurred source, then a luminosity layer that pins the
|
|
// brightness to the tint's (so light text stays readable over a white page, and dark text over
|
|
// a black one), then the tint's hue and saturation on top.
|
|
struct BackdropMaterial
|
|
{
|
|
enum class Source
|
|
{
|
|
// The windows behind this one, blurred (acrylic).
|
|
WindowsBehind,
|
|
// The desktop wallpaper, blurred, regardless of the windows in between (Mica). Needs
|
|
// Windows 11; elsewhere the windows behind are used.
|
|
Wallpaper,
|
|
};
|
|
Source source = Source::WindowsBehind;
|
|
// Only the RGB channels are used; the two opacities say how much of it shows.
|
|
D2D1_COLOR_F tint = {0.0f, 0.0f, 0.0f, 1.0f};
|
|
float tintOpacity = 0.0f;
|
|
float luminosityOpacity = 0.0f;
|
|
|
|
bool operator==(const BackdropMaterial &other) const
|
|
{
|
|
return source == other.source && tint.r == other.tint.r && tint.g == other.tint.g && tint.b == other.tint.b &&
|
|
tintOpacity == other.tintOpacity && luminosityOpacity == other.luminosityOpacity;
|
|
}
|
|
bool operator!=(const BackdropMaterial &other) const
|
|
{
|
|
return !(*this == other);
|
|
}
|
|
};
|
|
|
|
class DeviceResources
|
|
{
|
|
public:
|
|
using AnimationClock = std::chrono::steady_clock;
|
|
|
|
DeviceResources();
|
|
~DeviceResources();
|
|
DeviceResources(const DeviceResources &) = delete;
|
|
DeviceResources &operator=(const DeviceResources &) = delete;
|
|
|
|
// Marks the start of a frame. Brushes not used since the previous frame become
|
|
// evictable, and the clip stack used for culling starts over. The animation clock is
|
|
// sampled here, so every animated image in the frame shows the same moment, and the
|
|
// next-frame deadline starts over.
|
|
void BeginFrame();
|
|
|
|
// Axis-aligned clip that also feeds IsVisible(), so containers can skip children
|
|
// that cannot reach the screen. Pair every PushClip with a PopClip.
|
|
void PushClip(const RectF &rect, D2D1_ANTIALIAS_MODE mode = D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
|
|
void PopClip();
|
|
// False only when `rect` is entirely outside the current clip.
|
|
bool IsVisible(const RectF &rect) const;
|
|
|
|
bool EnsureFactories();
|
|
bool EnsureForWindow(HWND hwnd);
|
|
bool EnsureForComposition(HWND hwnd);
|
|
// Starts creating the Direct3D device on a background thread; the next composition
|
|
// target created in the process takes it instead of creating its own. Bringing up
|
|
// the GPU driver takes a few hundred milliseconds, so a window that calls this early
|
|
// gets that time back on its first frame.
|
|
static void PrewarmComposition();
|
|
// An opaque composition surface ignores alpha, which lets Direct2D draw ClearType
|
|
// text. Takes effect when the composition surface is next created.
|
|
void SetCompositionOpaque(bool opaque);
|
|
// Composes through Windows.UI.Composition instead of a DirectComposition swap chain:
|
|
// content is drawn into a CompositionDrawingSurface, and the system acrylic
|
|
// (HostBackdropBrush, the blurred desktop behind the window) can fill a rounded
|
|
// rectangle under it, see SetBackdrop. Content and backdrop shape go out in the same
|
|
// compositor commit, so a backdrop that follows a resizing card stays aligned with it;
|
|
// a swap chain present and a compositor commit are separate and visibly drift apart.
|
|
// Takes effect when the composition surface is next created. Needs Windows 10 1903;
|
|
// EnsureForComposition fails where it is missing.
|
|
void SetBackdropComposition(bool enabled);
|
|
bool UsesBackdropComposition() const;
|
|
// Rounded rectangle, in DIPs in the coordinates the scene draws in, that the backdrop
|
|
// fills. Call it inside a frame so it lands in the same commit as the content; an
|
|
// empty rect hides the backdrop. Ignored outside backdrop composition.
|
|
void SetBackdrop(const RectF &rectDips, float radiusDips);
|
|
// Material of the backdrop; kept across surface recreation. The default, both opacities
|
|
// 0, shows the blurred source as it is.
|
|
void SetBackdropMaterial(const BackdropMaterial &material);
|
|
// Opens and closes a frame on the current target. In backdrop composition the device
|
|
// context only exists between the two, so fetch GetRenderTarget() after BeginDraw.
|
|
// EndDraw returns D2DERR_RECREATE_TARGET when the device has to be recreated.
|
|
bool BeginDraw();
|
|
HRESULT EndDraw();
|
|
void Resize(UINT width, UINT height);
|
|
void DiscardTarget();
|
|
HRESULT Present();
|
|
|
|
/// Rendering DPI override for windows whose system DPI diverges from the
|
|
/// content's true scale (e.g. an RDP session syncs the client's display
|
|
/// scaling into the session's DPI metadata while the focused application
|
|
/// still renders at 96 DPI). Pass 0 (or any non-positive value) to clear
|
|
/// the override and fall back to GetDpiForWindow(hwnd). While set, the
|
|
/// override is the single DPI source so window sizing and rendering stay
|
|
/// on the same scale; a live render target is re-DPI'd immediately
|
|
/// because target recreation can be skipped for same-size compositions.
|
|
void SetDpiOverride(FLOAT dpi);
|
|
ID2D1RenderTarget *GetRenderTarget() const;
|
|
ID2D1DeviceContext *GetDeviceContext() const;
|
|
IDWriteFactory *GetDWriteFactory() const;
|
|
ID2D1SolidColorBrush *GetSolidColorBrush(const D2D1_COLOR_F &color);
|
|
IDWriteTextFormat *GetTextFormat(const std::wstring &fontFamily, float fontSize, DWRITE_FONT_WEIGHT fontWeight,
|
|
DWRITE_TEXT_ALIGNMENT textAlignment, DWRITE_PARAGRAPH_ALIGNMENT paragraphAlignment,
|
|
DWRITE_WORD_WRAPPING wordWrapping);
|
|
ID2D1Bitmap *GetBitmapFromFile(const std::wstring &filePath, D2D1_SIZE_F *size = nullptr);
|
|
// Like GetBitmapFromFile, but an animated image (APNG, animated GIF, multi-frame WebP)
|
|
// returns the frame for the current animation time and moves GetNextAnimationDeadline()
|
|
// to the moment that frame changes. Still images take the GetBitmapFromFile path. Sizes
|
|
// are in DIPs at 96 DPI, one DIP per image pixel, the same as GetBitmapFromFile.
|
|
ID2D1Bitmap *GetImageFrame(const std::wstring &filePath, D2D1_SIZE_F *size = nullptr);
|
|
// Drops every decoded image, still and animated, so files changed on disk are read again.
|
|
void ClearImageCaches();
|
|
// Off: images are drawn still, as GetBitmapFromFile decodes them (the APNG default image,
|
|
// the first GIF frame), and ask for no further frames. Hosts turn this off when the system
|
|
// asks for reduced motion.
|
|
void SetAnimationsEnabled(bool enabled);
|
|
bool AreAnimationsEnabled() const;
|
|
// Animations measure their elapsed time from this point; moving it restarts them all.
|
|
void SetAnimationEpoch(AnimationClock::time_point epoch);
|
|
// Overrides the time BeginFrame sampled, for hosts that keep their own frame clock.
|
|
void SetFrameTime(AnimationClock::time_point now);
|
|
// Earliest moment an animated image drawn since BeginFrame changes; empty when nothing
|
|
// drawn is animating. Hosts schedule their next frame for it.
|
|
std::optional<AnimationClock::time_point> GetNextAnimationDeadline() const;
|
|
// GPU memory all animated images may take together (every frame is a bitmap). An
|
|
// animation that does not fit on its own is shown still; others evict the least
|
|
// recently drawn animation.
|
|
void SetAnimatedBitmapBudget(size_t bytes);
|
|
size_t GetAnimatedBitmapBytes() const;
|
|
// Rasterizes an SVG document (UTF-8 markup) at `sizeDips` for the current DPI and
|
|
// caches the bitmap. Needs ID2D1DeviceContext5 (Windows 10 1703+); null otherwise.
|
|
ID2D1Bitmap *GetSvgBitmap(const std::string &markup, const SizeF &sizeDips);
|
|
bool UsesComposition() const;
|
|
|
|
private:
|
|
struct BrushCacheEntry
|
|
{
|
|
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
|
|
uint64_t lastFrame = 0;
|
|
};
|
|
|
|
struct SvgCacheEntry
|
|
{
|
|
size_t markupHash = 0;
|
|
std::string markup;
|
|
SizeF size = {};
|
|
float dpi = 0.0f;
|
|
Microsoft::WRL::ComPtr<ID2D1Bitmap> bitmap;
|
|
};
|
|
|
|
struct TextFormatCacheEntry
|
|
{
|
|
std::wstring fontFamily;
|
|
float fontSize = 0.0f;
|
|
DWRITE_FONT_WEIGHT fontWeight = DWRITE_FONT_WEIGHT_NORMAL;
|
|
DWRITE_TEXT_ALIGNMENT textAlignment = DWRITE_TEXT_ALIGNMENT_LEADING;
|
|
DWRITE_PARAGRAPH_ALIGNMENT paragraphAlignment = DWRITE_PARAGRAPH_ALIGNMENT_NEAR;
|
|
DWRITE_WORD_WRAPPING wordWrapping = DWRITE_WORD_WRAPPING_WRAP;
|
|
Microsoft::WRL::ComPtr<IDWriteTextFormat> format;
|
|
};
|
|
|
|
struct BitmapCacheEntry
|
|
{
|
|
std::wstring filePath;
|
|
D2D1_SIZE_F size = {};
|
|
Microsoft::WRL::ComPtr<ID2D1Bitmap> bitmap;
|
|
};
|
|
|
|
struct AnimatedBitmapEntry
|
|
{
|
|
std::wstring filePath;
|
|
D2D1_SIZE_F size = {};
|
|
std::vector<Microsoft::WRL::ComPtr<ID2D1Bitmap>> frames;
|
|
AnimationTimeline timeline;
|
|
size_t bytes = 0;
|
|
uint64_t lastFrame = 0;
|
|
};
|
|
|
|
AnimatedBitmapEntry *FindOrLoadAnimation(const std::wstring &filePath);
|
|
void RememberStillImage(const std::wstring &filePath);
|
|
void TrimAnimatedBitmaps(size_t incoming);
|
|
|
|
static constexpr size_t kMaxCachedBitmaps = 4;
|
|
static constexpr size_t kDefaultAnimatedBitmapBudget = 64ULL * 1024 * 1024;
|
|
static constexpr size_t kMaxRememberedStillImages = 64;
|
|
static constexpr size_t kMaxCachedSvgBitmaps = 96;
|
|
// Animated colors produce a new brush every frame; past this many, brushes left
|
|
// over from earlier frames are dropped.
|
|
static constexpr size_t kBrushCacheSoftLimit = 256;
|
|
|
|
static uint64_t ColorKey(const D2D1_COLOR_F &color);
|
|
void TrimBrushCache();
|
|
bool BindCompositionSurface();
|
|
bool CreateCompositionDevice();
|
|
bool EnsureSurfaceComposition(UINT width, UINT height);
|
|
bool GrowDrawingSurface(UINT width, UINT height);
|
|
bool ApplyBackdropMaterial();
|
|
FLOAT DpiForHwnd() const;
|
|
|
|
FLOAT dpiOverride_ = 0.0f;
|
|
|
|
HWND hwnd_ = nullptr;
|
|
UINT pixelWidth_ = 1;
|
|
UINT pixelHeight_ = 1;
|
|
bool composition_ = false;
|
|
bool compositionOpaque_ = false;
|
|
bool backdropComposition_ = false;
|
|
BackdropMaterial backdropMaterial_;
|
|
|
|
// Windows.UI.Composition objects of backdrop composition; kept out of this header so
|
|
// users of msimeui do not pull in the WinRT headers.
|
|
struct SurfaceComposition;
|
|
std::unique_ptr<SurfaceComposition> surface_;
|
|
// The drawing surface's device context, set only between BeginDraw and EndDraw.
|
|
Microsoft::WRL::ComPtr<ID2D1DeviceContext> drawContext_;
|
|
|
|
Microsoft::WRL::ComPtr<ID2D1Factory> d2dFactory_;
|
|
Microsoft::WRL::ComPtr<ID2D1Factory1> d2dFactory1_;
|
|
Microsoft::WRL::ComPtr<IDWriteFactory> dwriteFactory_;
|
|
Microsoft::WRL::ComPtr<IWICImagingFactory> wicFactory_;
|
|
Microsoft::WRL::ComPtr<ID2D1HwndRenderTarget> hwndRenderTarget_;
|
|
Microsoft::WRL::ComPtr<ID3D11Device> d3dDevice_;
|
|
Microsoft::WRL::ComPtr<IDXGIDevice> dxgiDevice_;
|
|
Microsoft::WRL::ComPtr<IDXGISwapChain1> swapChain_;
|
|
Microsoft::WRL::ComPtr<ID2D1Device> d2dDevice_;
|
|
Microsoft::WRL::ComPtr<ID2D1DeviceContext> deviceContext_;
|
|
Microsoft::WRL::ComPtr<ID2D1Bitmap1> dxgiBitmap_;
|
|
Microsoft::WRL::ComPtr<IDCompositionDevice> dcompDevice_;
|
|
Microsoft::WRL::ComPtr<IDCompositionTarget> dcompTarget_;
|
|
Microsoft::WRL::ComPtr<IDCompositionVisual> dcompVisual_;
|
|
std::unordered_map<uint64_t, BrushCacheEntry> brushCache_;
|
|
uint64_t frame_ = 0;
|
|
std::vector<TextFormatCacheEntry> textFormatCache_;
|
|
std::vector<BitmapCacheEntry> bitmapCache_;
|
|
std::vector<SvgCacheEntry> svgCache_;
|
|
std::vector<RectF> clipStack_;
|
|
// Animated images, decoded once per device. Paths found to be still images are
|
|
// remembered so they are not probed again; that is a property of the file, not of
|
|
// the device, so it survives DiscardTarget.
|
|
std::vector<AnimatedBitmapEntry> animatedCache_;
|
|
std::vector<std::wstring> stillImagePaths_;
|
|
size_t animatedBytes_ = 0;
|
|
size_t animatedBudget_ = kDefaultAnimatedBitmapBudget;
|
|
bool animationsEnabled_ = true;
|
|
AnimationClock::time_point animationEpoch_ = AnimationClock::now();
|
|
std::optional<AnimationClock::time_point> frameTime_;
|
|
std::optional<AnimationClock::time_point> nextAnimationDeadline_;
|
|
};
|
|
} // namespace msimeui
|