diff --git a/ui/include/msimeui/Layout.h b/ui/include/msimeui/Layout.h index 5a76c04c..49b54866 100644 --- a/ui/include/msimeui/Layout.h +++ b/ui/include/msimeui/Layout.h @@ -338,6 +338,9 @@ class Card : public Panel public: Card(Brush brush, float padding); + void SetBrush(Brush brush); + void SetPadding(float padding); + SizeF Measure(const SizeF &availableSize) override; void Arrange(const RectF &finalRect) override; void Render(DeviceResources &deviceResources) override; diff --git a/ui/src/Controls.cpp b/ui/src/Controls.cpp index 597f165e..9c8a2ee7 100644 --- a/ui/src/Controls.cpp +++ b/ui/src/Controls.cpp @@ -178,20 +178,51 @@ ComPtr CreateCachedTextLayout(IDWriteFactory *factory, const } ComPtr format; - if (FAILED(factory->CreateTextFormat(fontFamily.c_str(), nullptr, fontWeight, DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, fontSize, L"", format.GetAddressOf()))) + struct TextFormatKey { - if (FAILED(factory->CreateTextFormat(UiFontFallbackFamily(), nullptr, fontWeight, DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, fontSize, L"", format.GetAddressOf()))) + std::wstring family; + float size = 0.0f; + DWRITE_FONT_WEIGHT weight = 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_NO_WRAP; + ComPtr format; + }; + static std::vector formatCache; + for (auto &entry : formatCache) + { + if (entry.family == fontFamily && entry.size == fontSize && entry.weight == fontWeight && + entry.textAlignment == textAlignment && entry.paragraphAlignment == paragraphAlignment && + entry.wordWrapping == wordWrapping && entry.format) { - return layout; + format = entry.format; + break; } } - - format->SetTextAlignment(textAlignment); - format->SetParagraphAlignment(paragraphAlignment); - format->SetWordWrapping(wordWrapping); - ApplyUiFontFallback(factory, format.Get()); + if (!format) + { + if (FAILED(factory->CreateTextFormat(fontFamily.c_str(), nullptr, fontWeight, DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, fontSize, L"", format.GetAddressOf()))) + { + if (FAILED(factory->CreateTextFormat(UiFontFallbackFamily(), nullptr, fontWeight, DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, fontSize, L"", format.GetAddressOf()))) + { + return layout; + } + } + format->SetTextAlignment(textAlignment); + format->SetParagraphAlignment(paragraphAlignment); + format->SetWordWrapping(wordWrapping); + TextFormatKey entry; + entry.family = fontFamily; + entry.size = fontSize; + entry.weight = fontWeight; + entry.textAlignment = textAlignment; + entry.paragraphAlignment = paragraphAlignment; + entry.wordWrapping = wordWrapping; + entry.format = format; + formatCache.push_back(std::move(entry)); + } if (FAILED(factory->CreateTextLayout(text.c_str(), static_cast(text.size()), format.Get(), width, height, layout.GetAddressOf()))) diff --git a/ui/src/DeviceResources.cpp b/ui/src/DeviceResources.cpp index edaf4724..d3f98d2e 100644 --- a/ui/src/DeviceResources.cpp +++ b/ui/src/DeviceResources.cpp @@ -7,6 +7,16 @@ namespace msimeui { +namespace +{ +UINT AlignSwapExtent(UINT value) +{ + constexpr UINT kBucket = 64; + value = (std::max)(value, 1U); + return ((value + kBucket - 1U) / kBucket) * kBucket; +} +} // namespace + bool DeviceResources::IsSameColor(const D2D1_COLOR_F &lhs, const D2D1_COLOR_F &rhs) { return lhs.r == rhs.r && lhs.g == rhs.g && lhs.b == rhs.b && lhs.a == rhs.a; @@ -128,10 +138,10 @@ bool DeviceResources::EnsureForComposition(HWND hwnd) RECT rc = {}; GetClientRect(hwnd, &rc); - const UINT width = static_cast((std::max)(rc.right, 1L)); - const UINT height = static_cast((std::max)(rc.bottom, 1L)); + const UINT width = AlignSwapExtent(static_cast((std::max)(rc.right, 1L))); + const UINT height = AlignSwapExtent(static_cast((std::max)(rc.bottom, 1L))); - if (composition_ && deviceContext_ && swapChain_ && pixelWidth_ == width && pixelHeight_ == height) + if (composition_ && deviceContext_ && swapChain_ && pixelWidth_ >= width && pixelHeight_ >= height) { return true; } @@ -210,11 +220,13 @@ bool DeviceResources::EnsureForComposition(HWND hwnd) return true; } - pixelWidth_ = width; - pixelHeight_ = height; + const UINT newWidth = (std::max)(pixelWidth_, width); + const UINT newHeight = (std::max)(pixelHeight_, height); + pixelWidth_ = newWidth; + pixelHeight_ = newHeight; deviceContext_->SetTarget(nullptr); dxgiBitmap_.Reset(); - if (FAILED(swapChain_->ResizeBuffers(0, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, 0))) + if (FAILED(swapChain_->ResizeBuffers(0, newWidth, newHeight, DXGI_FORMAT_B8G8R8A8_UNORM, 0))) { return false; } @@ -232,13 +244,19 @@ void DeviceResources::Resize(UINT width, UINT height) pixelHeight_ = height; return; } - if (composition_ && swapChain_ && deviceContext_ && (pixelWidth_ != width || pixelHeight_ != height)) + if (composition_ && swapChain_ && deviceContext_) { - pixelWidth_ = width; - pixelHeight_ = height; + const UINT neededW = AlignSwapExtent(width); + const UINT neededH = AlignSwapExtent(height); + if (pixelWidth_ >= neededW && pixelHeight_ >= neededH) + { + return; + } + pixelWidth_ = (std::max)(pixelWidth_, neededW); + pixelHeight_ = (std::max)(pixelHeight_, neededH); deviceContext_->SetTarget(nullptr); dxgiBitmap_.Reset(); - if (SUCCEEDED(swapChain_->ResizeBuffers(0, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, 0))) + if (SUCCEEDED(swapChain_->ResizeBuffers(0, pixelWidth_, pixelHeight_, DXGI_FORMAT_B8G8R8A8_UNORM, 0))) { BindCompositionSurface(); } diff --git a/ui/src/Fonts.cpp b/ui/src/Fonts.cpp index 41e0a70d..5abae17d 100644 --- a/ui/src/Fonts.cpp +++ b/ui/src/Fonts.cpp @@ -61,26 +61,57 @@ void ApplyUiFontFallback(IDWriteFactory *factory, IDWriteTextFormat *format) { return; } - ComPtr builder; - if (FAILED(factory2->CreateFontFallbackBuilder(builder.GetAddressOf()))) + + static ComPtr cachedFallback; + static IDWriteFactory *cachedFactory = nullptr; + if (!cachedFallback || cachedFactory != factory) { - return; - } - WCHAR const *families[] = {L"Noto Sans SC", UiFontFallbackFamily()}; - DWRITE_UNICODE_RANGE allText{0, 0x10FFFF}; - if (FAILED(builder->AddMapping(&allText, 1, families, 2, nullptr, nullptr, nullptr, 1.0f))) - { - return; - } - ComPtr customFallback; - if (FAILED(builder->CreateFontFallback(customFallback.GetAddressOf()))) - { - return; + ComPtr builder; + if (FAILED(factory2->CreateFontFallbackBuilder(builder.GetAddressOf()))) + { + return; + } + + // Only CJK: mapping all of Unicode to Noto/YaHei replaces system fallback + // and makes Segoe UI Emoji (COLR) tofu. + static const DWRITE_UNICODE_RANGE kCjk[] = { + {0x2E80, 0x2EFF}, {0x2F00, 0x2FDF}, {0x3000, 0x303F}, {0x3040, 0x30FF}, + {0x3100, 0x312F}, {0x31A0, 0x31BF}, {0x31C0, 0x31EF}, {0x3200, 0x32FF}, + {0x3300, 0x33FF}, {0x3400, 0x4DBF}, {0x4E00, 0x9FFF}, {0xF900, 0xFAFF}, + {0xFF00, 0xFFEF}, {0x20000, 0x2A6DF}, {0x2A700, 0x2B73F}, {0x2B740, 0x2B81F}, + {0x2B820, 0x2CEAF}, {0x2CEB0, 0x2EBEF}, {0x30000, 0x3134F}, + }; + WCHAR const *cjkFamilies[] = {L"Noto Sans SC", UiFontFallbackFamily()}; + if (FAILED(builder->AddMapping(kCjk, static_cast(sizeof(kCjk) / sizeof(kCjk[0])), cjkFamilies, 2, + nullptr, nullptr, nullptr, 1.0f))) + { + return; + } + + if (FontFamilyExists(factory, L"Segoe UI Emoji")) + { + static const DWRITE_UNICODE_RANGE kEmoji[] = { + {0x200D, 0x200D}, {0x2600, 0x27BF}, {0xFE00, 0xFE0F}, {0x1F000, 0x1F02F}, + {0x1F0A0, 0x1F0FF}, {0x1F300, 0x1FAFF}, {0x1F1E6, 0x1F1FF}, + }; + WCHAR const *emojiFamilies[] = {L"Segoe UI Emoji"}; + builder->AddMapping(kEmoji, static_cast(sizeof(kEmoji) / sizeof(kEmoji[0])), emojiFamilies, 1, + nullptr, nullptr, nullptr, 1.0f); + } + + ComPtr customFallback; + if (FAILED(builder->CreateFontFallback(customFallback.GetAddressOf()))) + { + return; + } + cachedFallback = customFallback; + cachedFactory = factory; } + ComPtr format1; if (SUCCEEDED(format->QueryInterface(IID_PPV_ARGS(&format1)))) { - format1->SetFontFallback(customFallback.Get()); + format1->SetFontFallback(cachedFallback.Get()); } } } // namespace msimeui diff --git a/ui/src/Layout.cpp b/ui/src/Layout.cpp index 8dc87e3b..56f2f3e4 100644 --- a/ui/src/Layout.cpp +++ b/ui/src/Layout.cpp @@ -204,100 +204,31 @@ bool IsSameRect(const RectF &lhs, const RectF &rhs) return lhs.x == rhs.x && lhs.y == rhs.y && lhs.width == rhs.width && lhs.height == rhs.height; } -void DrawLayeredMistShadow(ID2D1RenderTarget *target, const RectF &bounds, float radius) -{ - for (int i = 1; i <= 20; ++i) - { - const float t = static_cast(i) / 20.0f; - const float spread = 1.15f * static_cast(i); - const float offsetY = 0.4f * static_cast(i); - const float alpha = 0.14f * (1.0f - t) * (1.0f - t); - ComPtr brush; - if (FAILED(target->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0, alpha), brush.GetAddressOf()))) - { - continue; - } - const auto rounded = D2D1::RoundedRect( - D2D1::RectF(bounds.x - spread, bounds.y - spread + offsetY, bounds.x + bounds.width + spread, - bounds.y + bounds.height + spread + offsetY), - radius + spread, radius + spread); - target->FillRoundedRectangle(rounded, brush.Get()); - } -} - -bool DrawGaussianMistShadow(ID2D1RenderTarget *target, const RectF &bounds, float radius) -{ - ComPtr dc; - if (FAILED(target->QueryInterface(IID_PPV_ARGS(dc.GetAddressOf())))) - { - return false; - } - - auto drawPass = [&](float stdDeviation, float alpha, float offsetY) -> bool { - const float pad = stdDeviation * 3.0f + 4.0f; - const D2D1_SIZE_F bitmapSize = {bounds.width + pad * 2.0f, bounds.height + pad * 2.0f}; - if (bitmapSize.width < 2.0f || bitmapSize.height < 2.0f) - { - return false; - } - - ComPtr compatible; - if (FAILED(target->CreateCompatibleRenderTarget(bitmapSize, compatible.GetAddressOf()))) - { - return false; - } - - compatible->BeginDraw(); - compatible->Clear(D2D1::ColorF(0, 0.0f)); - ComPtr fill; - if (FAILED(compatible->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0, alpha), fill.GetAddressOf()))) - { - compatible->EndDraw(); - return false; - } - const auto shape = D2D1::RoundedRect( - D2D1::RectF(pad, pad, pad + bounds.width, pad + bounds.height), radius, radius); - compatible->FillRoundedRectangle(shape, fill.Get()); - if (FAILED(compatible->EndDraw())) - { - return false; - } - - ComPtr bitmap; - if (FAILED(compatible->GetBitmap(bitmap.GetAddressOf()))) - { - return false; - } - - ComPtr blur; - if (FAILED(dc->CreateEffect(CLSID_D2D1GaussianBlur, blur.GetAddressOf()))) - { - return false; - } - blur->SetInput(0, bitmap.Get()); - blur->SetValue(D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, stdDeviation); - blur->SetValue(D2D1_GAUSSIANBLUR_PROP_BORDER_MODE, D2D1_BORDER_MODE_SOFT); - blur->SetValue(D2D1_GAUSSIANBLUR_PROP_OPTIMIZATION, D2D1_GAUSSIANBLUR_OPTIMIZATION_QUALITY); - dc->DrawImage(blur.Get(), D2D1::Point2F(bounds.x - pad, bounds.y - pad + offsetY)); - return true; - }; - - // Win11: wide ambient fog + tighter contact shadow, mostly downward. - const bool ambient = drawPass(11.0f, 0.42f, 3.0f); - const bool mid = drawPass(6.0f, 0.30f, 4.0f); - const bool contact = drawPass(2.8f, 0.48f, 3.0f); - return ambient || mid || contact; -} - void DrawWin11WindowShadow(ID2D1RenderTarget *target, const RectF &bounds, float radius) { if (!target || bounds.width <= 0.0f || bounds.height <= 0.0f) { return; } - if (!DrawGaussianMistShadow(target, bounds, radius)) + // Soft layered fills only. GaussianBlur + CreateCompatibleRenderTarget during + // BeginDraw retains GPU working-set across keystrokes (swapchain-sized + // intermediates are not reliably released by the D2D effect graph). + static const float kSpreads[] = {3.0f, 7.0f, 12.0f, 18.0f, 24.0f}; + static const float kAlphas[] = {0.22f, 0.14f, 0.09f, 0.05f, 0.03f}; + static const float kOffsetY[] = {1.0f, 2.0f, 3.5f, 5.0f, 7.0f}; + for (int i = 0; i < 5; ++i) { - DrawLayeredMistShadow(target, bounds, radius); + ComPtr brush; + if (FAILED(target->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0, kAlphas[i]), brush.GetAddressOf()))) + { + continue; + } + const float spread = kSpreads[i]; + const auto rounded = D2D1::RoundedRect( + D2D1::RectF(bounds.x - spread, bounds.y - spread + kOffsetY[i], bounds.x + bounds.width + spread, + bounds.y + bounds.height + spread + kOffsetY[i]), + radius + spread, radius + spread); + target->FillRoundedRectangle(rounded, brush.Get()); } } } // namespace @@ -1768,6 +1699,22 @@ Card::Card(Brush brush, float padding) : brush_(brush), padding_(padding) { } +void Card::SetBrush(Brush brush) +{ + brush_ = brush; + InvalidateVisual(); +} + +void Card::SetPadding(float padding) +{ + if (padding_ == padding) + { + return; + } + padding_ = padding; + InvalidateMeasure(); +} + SizeF Card::Measure(const SizeF &availableSize) { const SizeF inner = {std::max(availableSize.width - padding_ * 2.0f, 0.0f),