diff --git a/ui/include/msimeui/Controls.h b/ui/include/msimeui/Controls.h index 9aec4173..6a485253 100644 --- a/ui/include/msimeui/Controls.h +++ b/ui/include/msimeui/Controls.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -630,10 +631,20 @@ class CandidateList : public Visual size_t HitTestItem(const PointF &point) const; float EstimateTextWidth(const std::wstring &text, float fontSize) const; RectF ItemRect(size_t index) const; + const std::wstring &ResolvedFontFamily() const; + // Results of EstimateTextWidth / MeasureTextHeight. A frame measures the + // list several times (natural size, sticky size, arrange at the final + // width) and each measurement used to build a fresh DirectWrite layout per + // string; the metrics depend only on the text, size, width and font, so + // they are memoized until the font changes. + bool LookupTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width, float &value) const; + void StoreTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width, float value) const; std::vector items_; std::vector layoutCache_; std::vector itemGeometry_; + mutable std::unordered_map textMetricCache_; + mutable std::wstring textMetricFamily_; float layoutWidth_ = 0.0f; Appearance appearance_{}; Orientation orientation_ = Orientation::Vertical; diff --git a/ui/include/msimeui/Layout.h b/ui/include/msimeui/Layout.h index 5c677d42..6ef90de5 100644 --- a/ui/include/msimeui/Layout.h +++ b/ui/include/msimeui/Layout.h @@ -368,6 +368,9 @@ class Card : public Panel void Render(DeviceResources &deviceResources) override; private: + void RenderShadow(ID2D1RenderTarget *target); + bool RenderCachedShadow(ID2D1RenderTarget *target, const ShadowPass *passes, size_t count); + Brush brush_; float padding_ = 0.0f; float shadowScale_ = 1.0f; @@ -375,6 +378,21 @@ class Card : public Panel std::vector shadowPasses_; bool shadowEnabled_ = true; SizeF childSize_ = {}; + + // All blur passes composed into one bitmap. Rebuilding the blur inputs and + // effects on every frame dominated repaints that do not change the card's + // size (selection moves, late candidate enrichment); the bitmap is reused + // while the key below matches and belongs to cachedShadowTarget_. + Microsoft::WRL::ComPtr cachedShadowTarget_; + Microsoft::WRL::ComPtr cachedShadowBitmap_; + SizeF cachedShadowSize_ = {}; + float cachedShadowRadius_ = 0.0f; + float cachedShadowScale_ = 0.0f; + float cachedShadowOpacity_ = 0.0f; + float cachedShadowDpiX_ = 0.0f; + float cachedShadowDpiY_ = 0.0f; + float cachedShadowPad_ = 0.0f; + std::vector cachedShadowPasses_; }; class TextBlock : public Visual @@ -399,6 +417,7 @@ class TextBlock : public Visual private: void InvalidateTextLayoutCache(); + void InvalidateTextFormatCache(); std::wstring text_; float fontSize_ = 16.0f; @@ -417,6 +436,11 @@ class TextBlock : public Visual std::wstring cachedFontFamily_; float cachedLayoutWidth_ = -1.0f; Microsoft::WRL::ComPtr cachedTextLayout_; + // The format (family, size, weight, alignment, fallback) outlives text and + // caret changes; building it, and a custom fallback chain in particular, + // cost more than the layout itself on every keystroke. + std::wstring cachedFormatFamily_; + Microsoft::WRL::ComPtr cachedTextFormat_; }; class Spacer : public Visual diff --git a/ui/src/Controls.cpp b/ui/src/Controls.cpp index a9a1765a..4fc420cb 100644 --- a/ui/src/Controls.cpp +++ b/ui/src/Controls.cpp @@ -7,7 +7,9 @@ #include #include +#include #include +#include #include #include #include @@ -2705,9 +2707,70 @@ void CandidateList::SetAppearance(Appearance appearance) { appearance_ = appearance; InvalidateLayoutCache(); + textMetricCache_.clear(); InvalidateMeasure(); } +const std::wstring &CandidateList::ResolvedFontFamily() const +{ + const Theme &theme = ThemeManager::GetCurrent(); + return appearance_.fontFamily.empty() ? theme.textInputFontFamily : appearance_.fontFamily; +} + +namespace +{ +std::wstring TextMetricKey(wchar_t kind, const std::wstring &text, float fontSize, float width) +{ + uint32_t sizeBits = 0; + uint32_t widthBits = 0; + static_assert(sizeof(float) == sizeof(uint32_t)); + std::memcpy(&sizeBits, &fontSize, sizeof(sizeBits)); + std::memcpy(&widthBits, &width, sizeof(widthBits)); + std::wstring key; + key.reserve(text.size() + 5); + key.push_back(kind); + key.push_back(static_cast(sizeBits & 0xFFFF)); + key.push_back(static_cast(sizeBits >> 16)); + key.push_back(static_cast(widthBits & 0xFFFF)); + key.push_back(static_cast(widthBits >> 16)); + key.append(text); + return key; +} + +// Enough for several pages of candidates with labels, annotations and +// translations; past it the cache simply starts over. +constexpr size_t kMaxTextMetricEntries = 2048; +} // namespace + +bool CandidateList::LookupTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width, + float &value) const +{ + const std::wstring &family = ResolvedFontFamily(); + if (textMetricFamily_ != family) + { + textMetricCache_.clear(); + textMetricFamily_ = family; + return false; + } + const auto found = textMetricCache_.find(TextMetricKey(kind, text, fontSize, width)); + if (found == textMetricCache_.end()) + { + return false; + } + value = found->second; + return true; +} + +void CandidateList::StoreTextMetric(wchar_t kind, const std::wstring &text, float fontSize, float width, + float value) const +{ + if (textMetricCache_.size() >= kMaxTextMetricEntries) + { + textMetricCache_.clear(); + } + textMetricCache_[TextMetricKey(kind, text, fontSize, width)] = value; +} + void CandidateList::SetOrientation(Orientation orientation) { orientation_ = orientation; @@ -2732,10 +2795,14 @@ float CandidateList::EstimateTextWidth(const std::wstring &text, float fontSize) return 0.0f; } + float cached = 0.0f; + if (LookupTextMetric(L'w', text, fontSize, 0.0f, cached)) + { + return cached; + } + IDWriteFactory *factory = GetSharedDWriteFactory(); - const Theme &theme = ThemeManager::GetCurrent(); - const std::wstring &fontFamily = - appearance_.fontFamily.empty() ? theme.textInputFontFamily : appearance_.fontFamily; + const std::wstring &fontFamily = ResolvedFontFamily(); ComPtr layout = CreateCachedTextLayout( factory, fontFamily, text, fontSize, DWRITE_FONT_WEIGHT_NORMAL, 4096.0f, std::max(fontSize * 2.0f, 1.0f), DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_CENTER, DWRITE_WORD_WRAPPING_NO_WRAP, @@ -2754,22 +2821,29 @@ float CandidateList::EstimateTextWidth(const std::wstring &text, float fontSize) layout->GetMetrics(&metrics); DWRITE_OVERHANG_METRICS overhang = {}; layout->GetOverhangMetrics(&overhang); - return std::ceil(metrics.widthIncludingTrailingWhitespace + std::max(overhang.right, 0.0f) + 1.0f); + const float width = std::ceil(metrics.widthIncludingTrailingWhitespace + std::max(overhang.right, 0.0f) + 1.0f); + StoreTextMetric(L'w', text, fontSize, 0.0f, width); + return width; } float CandidateList::MeasureTextHeight(const std::wstring &text, float fontSize, float width) const { if (text.empty()) return 0.0f; - const Theme &theme = ThemeManager::GetCurrent(); - const auto &family = appearance_.fontFamily.empty() ? theme.textInputFontFamily : appearance_.fontFamily; - const auto layout = - CreateCachedTextLayout(GetSharedDWriteFactory(), family, text, fontSize, DWRITE_FONT_WEIGHT_NORMAL, width, - 65536.0f, DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_NEAR, - DWRITE_WORD_WRAPPING_WRAP, appearance_.fallbackFontFamilies); + float cached = 0.0f; + if (LookupTextMetric(L'h', text, fontSize, width, cached)) + return cached; + const auto layout = CreateCachedTextLayout(GetSharedDWriteFactory(), ResolvedFontFamily(), text, fontSize, + DWRITE_FONT_WEIGHT_NORMAL, width, 65536.0f, + DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_NEAR, + DWRITE_WORD_WRAPPING_WRAP, appearance_.fallbackFontFamilies); DWRITE_TEXT_METRICS metrics{}; if (layout && SUCCEEDED(layout->GetMetrics(&metrics))) - return std::ceil(metrics.height); + { + const float height = std::ceil(metrics.height); + StoreTextMetric(L'h', text, fontSize, width, height); + return height; + } return std::ceil(EstimateTextWidth(text, fontSize) / width) * fontSize * 1.25f; } @@ -3017,7 +3091,8 @@ void CandidateList::Render(DeviceResources &deviceResources) cache.textWidth = textRect.width; cache.fontFamily = fontFamily; } - if (cache.fontFamily != fontFamily || cache.annotationWidth != annotationRect.width) + if (!items_[index].annotation.empty() && + (cache.fontFamily != fontFamily || cache.annotationWidth != annotationRect.width)) { cache.annotationLayout = CreateCachedTextLayout( factory, fontFamily, items_[index].annotation, appearance_.annotationFontSize, @@ -3027,7 +3102,8 @@ void CandidateList::Render(DeviceResources &deviceResources) cache.annotationWidth = annotationRect.width; cache.fontFamily = fontFamily; } - if (cache.fontFamily != fontFamily || cache.translationWidth != translationRect.width) + if (!items_[index].translation.empty() && + (cache.fontFamily != fontFamily || cache.translationWidth != translationRect.width)) { cache.translationLayout = CreateCachedTextLayout( factory, fontFamily, items_[index].translation, translationFontSize, DWRITE_FONT_WEIGHT_NORMAL, diff --git a/ui/src/Layout.cpp b/ui/src/Layout.cpp index a702b32f..821755b6 100644 --- a/ui/src/Layout.cpp +++ b/ui/src/Layout.cpp @@ -1912,15 +1912,7 @@ void Card::Render(DeviceResources &deviceResources) if (shadowEnabled_) { - if (shadowPasses_.empty()) - { - DrawWin11WindowShadow(target, bounds_, brush_.radiusX, shadowScale_, shadowOpacity_); - } - else - { - DrawGaussianPasses(target, bounds_, brush_.radiusX, shadowPasses_.data(), shadowPasses_.size(), - shadowScale_, shadowOpacity_); - } + RenderShadow(target); } const auto roundedRect = @@ -1935,6 +1927,110 @@ void Card::Render(DeviceResources &deviceResources) } } +void Card::RenderShadow(ID2D1RenderTarget *target) +{ + const bool builtIn = shadowPasses_.empty(); + const ShadowPass *passes = builtIn ? kMistShadowPasses : shadowPasses_.data(); + const size_t count = builtIn ? std::size(kMistShadowPasses) : shadowPasses_.size(); + if (RenderCachedShadow(target, passes, count)) + { + return; + } + + // Uncached path, unchanged from before the cache existed. + cachedShadowBitmap_.Reset(); + cachedShadowTarget_.Reset(); + if (builtIn) + { + DrawWin11WindowShadow(target, bounds_, brush_.radiusX, shadowScale_, shadowOpacity_); + } + else + { + DrawGaussianPasses(target, bounds_, brush_.radiusX, passes, count, shadowScale_, shadowOpacity_); + } +} + +bool Card::RenderCachedShadow(ID2D1RenderTarget *target, const ShadowPass *passes, size_t count) +{ + if (bounds_.width <= 0.0f || bounds_.height <= 0.0f || count == 0) + { + return false; + } + + float dpiX = 96.0f; + float dpiY = 96.0f; + target->GetDpi(&dpiX, &dpiY); + const bool hit = cachedShadowBitmap_ && cachedShadowTarget_.Get() == target && + IsSameSize(cachedShadowSize_, {bounds_.width, bounds_.height}) && + cachedShadowRadius_ == brush_.radiusX && cachedShadowScale_ == shadowScale_ && + cachedShadowOpacity_ == shadowOpacity_ && cachedShadowDpiX_ == dpiX && cachedShadowDpiY_ == dpiY && + cachedShadowPasses_.size() == count && + std::equal(cachedShadowPasses_.begin(), cachedShadowPasses_.end(), passes); + if (!hit) + { + cachedShadowBitmap_.Reset(); + cachedShadowTarget_.Reset(); + + const float s = std::max(shadowScale_, 0.15f); + float pad = 0.0f; + for (size_t i = 0; i < count; ++i) + { + // Same margin DrawGaussianShadowPass gives each pass, plus that pass's offset. + const float passPad = passes[i].sigma * s * 3.0f + 4.0f + + std::max(std::fabs(passes[i].offsetX), std::fabs(passes[i].offsetY)) * s; + pad = std::max(pad, passPad); + } + pad = std::ceil(pad); + + ComPtr composed; + if (FAILED(target->CreateCompatibleRenderTarget({bounds_.width + pad * 2.0f, bounds_.height + pad * 2.0f}, + composed.GetAddressOf()))) + { + return false; + } + ComPtr composedDc; + if (FAILED(composed.As(&composedDc))) + { + return false; + } + composed->BeginDraw(); + composed->Clear(D2D1::ColorF(0, 0.0f)); + const RectF local = {pad, pad, bounds_.width, bounds_.height}; + bool drew = false; + for (size_t i = 0; i < count; ++i) + { + if (DrawGaussianShadowPass(composed.Get(), composedDc.Get(), local, brush_.radiusX, passes[i].sigma * s, + passes[i].alpha * shadowOpacity_, passes[i].offsetX * s, passes[i].offsetY * s)) + { + drew = true; + } + } + ComPtr bitmap; + if (FAILED(composed->EndDraw()) || !drew || FAILED(composed->GetBitmap(bitmap.GetAddressOf()))) + { + return false; + } + + cachedShadowTarget_ = target; + cachedShadowBitmap_ = bitmap; + cachedShadowSize_ = {bounds_.width, bounds_.height}; + cachedShadowRadius_ = brush_.radiusX; + cachedShadowScale_ = shadowScale_; + cachedShadowOpacity_ = shadowOpacity_; + cachedShadowDpiX_ = dpiX; + cachedShadowDpiY_ = dpiY; + cachedShadowPad_ = pad; + cachedShadowPasses_.assign(passes, passes + count); + } + + const float pad = cachedShadowPad_; + target->DrawBitmap(cachedShadowBitmap_.Get(), + D2D1::RectF(bounds_.x - pad, bounds_.y - pad, bounds_.x + bounds_.width + pad, + bounds_.y + bounds_.height + pad), + 1.0f, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR); + return true; +} + TextBlock::TextBlock(std::wstring text, float fontSize, D2D1_COLOR_F color, bool bold) : text_(std::move(text)), fontSize_(fontSize), color_(color), caretColor_(color), bold_(bold) { @@ -1960,7 +2056,7 @@ void TextBlock::SetFontSize(float fontSize) return; } fontSize_ = fontSize; - InvalidateTextLayoutCache(); + InvalidateTextFormatCache(); InvalidateMeasure(); } @@ -1972,7 +2068,7 @@ void TextBlock::SetTextAlignment(DWRITE_TEXT_ALIGNMENT alignment) } textAlignment_ = alignment; - InvalidateTextLayoutCache(); + InvalidateTextFormatCache(); InvalidateVisual(); } @@ -1984,7 +2080,7 @@ void TextBlock::SetFontFamily(std::wstring fontFamily) } fontFamilyOverride_ = std::move(fontFamily); - InvalidateTextLayoutCache(); + InvalidateTextFormatCache(); InvalidateMeasure(); } @@ -1995,13 +2091,20 @@ void TextBlock::InvalidateTextLayoutCache() cachedLayoutWidth_ = -1.0f; } +void TextBlock::InvalidateTextFormatCache() +{ + cachedTextFormat_.Reset(); + cachedFormatFamily_.clear(); + InvalidateTextLayoutCache(); +} + void TextBlock::SetFallbackFontFamilies(std::vector families) { if (hasCustomFontFallback_ && fallbackFontFamilies_ == families) return; hasCustomFontFallback_ = true; fallbackFontFamilies_ = std::move(families); - InvalidateTextLayoutCache(); + InvalidateTextFormatCache(); InvalidateMeasure(); } @@ -2066,26 +2169,33 @@ SizeF TextBlock::Measure(const SizeF &availableSize) const bool needsLayout = !cachedTextLayout_ || cachedLayoutWidth_ != maxWidth || cachedFontFamily_ != fontFamily; if (needsLayout) { - ComPtr format; - if (FAILED(dwriteFactory->CreateTextFormat( - fontFamily.c_str(), nullptr, bold_ ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL, - DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, fontSize_, L"", format.GetAddressOf()))) + if (!cachedTextFormat_ || cachedFormatFamily_ != fontFamily) { + ComPtr format; if (FAILED(dwriteFactory->CreateTextFormat( - UiFontFallbackFamily(), nullptr, bold_ ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL, + fontFamily.c_str(), nullptr, bold_ ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, fontSize_, L"", format.GetAddressOf()))) { - measured_ = {maxWidth, fontSize_ + textLayoutPadding_.top + textLayoutPadding_.bottom}; - return measured_; + if (FAILED(dwriteFactory->CreateTextFormat( + UiFontFallbackFamily(), nullptr, + bold_ ? DWRITE_FONT_WEIGHT_SEMI_BOLD : DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, fontSize_, L"", format.GetAddressOf()))) + { + measured_ = {maxWidth, fontSize_ + textLayoutPadding_.top + textLayoutPadding_.bottom}; + return measured_; + } } - } - format->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP); - format->SetTextAlignment(textAlignment_); - if (hasCustomFontFallback_) - ApplyFontFallback(dwriteFactory, format.Get(), fallbackFontFamilies_); - else - ApplyUiFontFallback(dwriteFactory, format.Get()); + format->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP); + format->SetTextAlignment(textAlignment_); + if (hasCustomFontFallback_) + ApplyFontFallback(dwriteFactory, format.Get(), fallbackFontFamilies_); + else + ApplyUiFontFallback(dwriteFactory, format.Get()); + cachedTextFormat_ = format; + cachedFormatFamily_ = fontFamily; + } + IDWriteTextFormat *format = cachedTextFormat_.Get(); const UINT32 caretPos = static_cast((std::min)(caretIndex_, text_.size())); const bool insertSlot = showCaret_ && caretPos < text_.size(); @@ -2095,8 +2205,8 @@ SizeF TextBlock::Measure(const SizeF &availableSize) layoutText.insert(layoutText.begin() + static_cast(caretPos), kCaretSlotChar); } - if (FAILED(dwriteFactory->CreateTextLayout(layoutText.c_str(), static_cast(layoutText.size()), - format.Get(), maxWidth, std::numeric_limits::max(), + if (FAILED(dwriteFactory->CreateTextLayout(layoutText.c_str(), static_cast(layoutText.size()), format, + maxWidth, std::numeric_limits::max(), cachedTextLayout_.ReleaseAndGetAddressOf()))) { measured_ = {maxWidth, fontSize_ + textLayoutPadding_.top + textLayoutPadding_.bottom};