mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-11 04:38:24 -06:00
This adds support for specifying more than one font family using a syntax that is similar to CSS' `font-family` property. The implementation is straight-forward and is effectively just a wrapper around `IDWriteFontFallbackBuilder`. Closes #2664 ## PR Checklist * Font fallback * Write "「猫」" * Use "Consolas" and remember the shape of the glyphs * Use "Consolas, MS Gothic" and check that it changed ✅ * Settings UI autocompletion * It completes ✅ * It filters ✅ * It recognizes commas and starts a new name ✅ * All invalid font names are listed in the warning message ✅ --------- Co-authored-by: Dustin L. Howett <duhowett@microsoft.com>
1093 lines
45 KiB
C++
1093 lines
45 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "AtlasEngine.h"
|
|
|
|
#include <til/unicode.h>
|
|
|
|
#include "Backend.h"
|
|
#include "BuiltinGlyphs.h"
|
|
#include "DWriteTextAnalysis.h"
|
|
#include "../../interactivity/win32/CustomWindowMessages.h"
|
|
|
|
// #### NOTE ####
|
|
// This file should only contain methods that are only accessed by the caller of Present() (the "Renderer" class).
|
|
// Basically this file poses the "synchronization" point between the concurrently running
|
|
// general IRenderEngine API (like the Invalidate*() methods) and the Present() method
|
|
// and thus may access both _r and _api.
|
|
|
|
#pragma warning(disable : 4100) // '...': unreferenced formal parameter
|
|
// Disable a bunch of warnings which get in the way of writing performant code.
|
|
#pragma warning(disable : 26429) // Symbol 'data' is never tested for nullness, it can be marked as not_null (f.23).
|
|
#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
|
|
#pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...].
|
|
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
|
|
#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1).
|
|
#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
|
|
|
|
using namespace Microsoft::Console::Render::Atlas;
|
|
|
|
#pragma warning(suppress : 26455) // Default constructor may not throw. Declare it 'noexcept' (f.6).
|
|
AtlasEngine::AtlasEngine()
|
|
{
|
|
#ifdef NDEBUG
|
|
THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(_p.d2dFactory), nullptr, reinterpret_cast<void**>(_p.d2dFactory.addressof())));
|
|
#else
|
|
static constexpr D2D1_FACTORY_OPTIONS options{ .debugLevel = D2D1_DEBUG_LEVEL_INFORMATION };
|
|
THROW_IF_FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(_p.d2dFactory), &options, reinterpret_cast<void**>(_p.d2dFactory.addressof())));
|
|
#endif
|
|
|
|
THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(_p.dwriteFactory), reinterpret_cast<::IUnknown**>(_p.dwriteFactory.addressof())));
|
|
_p.dwriteFactory4 = _p.dwriteFactory.try_query<IDWriteFactory4>();
|
|
|
|
THROW_IF_FAILED(_p.dwriteFactory->GetSystemFontFallback(_api.systemFontFallback.addressof()));
|
|
|
|
wil::com_ptr<IDWriteTextAnalyzer> textAnalyzer;
|
|
THROW_IF_FAILED(_p.dwriteFactory->CreateTextAnalyzer(textAnalyzer.addressof()));
|
|
_p.textAnalyzer = textAnalyzer.query<IDWriteTextAnalyzer1>();
|
|
}
|
|
|
|
#pragma region IRenderEngine
|
|
|
|
// StartPaint() is called while the console buffer lock is being held.
|
|
// --> Put as little in here as possible.
|
|
[[nodiscard]] HRESULT AtlasEngine::StartPaint() noexcept
|
|
try
|
|
{
|
|
if (const auto hwnd = _api.s->target->hwnd)
|
|
{
|
|
RECT rect;
|
|
LOG_IF_WIN32_BOOL_FALSE(GetClientRect(hwnd, &rect));
|
|
std::ignore = SetWindowSize({ rect.right - rect.left, rect.bottom - rect.top });
|
|
|
|
if (_api.invalidatedTitle)
|
|
{
|
|
LOG_IF_WIN32_BOOL_FALSE(PostMessageW(hwnd, CM_UPDATE_TITLE, 0, 0));
|
|
_api.invalidatedTitle = false;
|
|
}
|
|
}
|
|
|
|
if (_p.s != _api.s)
|
|
{
|
|
_handleSettingsUpdate();
|
|
}
|
|
|
|
if (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION || _hackTriggerRedrawAll)
|
|
{
|
|
_hackTriggerRedrawAll = false;
|
|
_api.invalidatedRows = invalidatedRowsAll;
|
|
_api.scrollOffset = 0;
|
|
}
|
|
|
|
// Clamp invalidation rects into valid value ranges.
|
|
{
|
|
_api.invalidatedCursorArea.left = std::min(_api.invalidatedCursorArea.left, _p.s->viewportCellCount.x);
|
|
_api.invalidatedCursorArea.top = std::min(_api.invalidatedCursorArea.top, _p.s->viewportCellCount.y);
|
|
_api.invalidatedCursorArea.right = clamp(_api.invalidatedCursorArea.right, _api.invalidatedCursorArea.left, _p.s->viewportCellCount.x);
|
|
_api.invalidatedCursorArea.bottom = clamp(_api.invalidatedCursorArea.bottom, _api.invalidatedCursorArea.top, _p.s->viewportCellCount.y);
|
|
}
|
|
{
|
|
_api.invalidatedRows.start = std::min(_api.invalidatedRows.start, _p.s->viewportCellCount.y);
|
|
_api.invalidatedRows.end = clamp(_api.invalidatedRows.end, _api.invalidatedRows.start, _p.s->viewportCellCount.y);
|
|
}
|
|
if (_api.scrollOffset)
|
|
{
|
|
const auto limit = gsl::narrow_cast<i16>(_p.s->viewportCellCount.y & 0x7fff);
|
|
const auto offset = gsl::narrow_cast<i16>(clamp<int>(_api.scrollOffset, -limit, limit));
|
|
const auto nothingInvalid = _api.invalidatedRows.start == _api.invalidatedRows.end;
|
|
|
|
_api.scrollOffset = offset;
|
|
|
|
// Mark the newly scrolled in rows as invalidated
|
|
if (offset < 0)
|
|
{
|
|
const u16 begRow = _p.s->viewportCellCount.y + offset;
|
|
_api.invalidatedRows.start = nothingInvalid ? begRow : std::min(_api.invalidatedRows.start, begRow);
|
|
_api.invalidatedRows.end = _p.s->viewportCellCount.y;
|
|
}
|
|
else
|
|
{
|
|
const u16 endRow = offset;
|
|
_api.invalidatedRows.start = 0;
|
|
_api.invalidatedRows.end = nothingInvalid ? endRow : std::max(_api.invalidatedRows.end, endRow);
|
|
}
|
|
}
|
|
|
|
_api.dirtyRect = {
|
|
0,
|
|
_api.invalidatedRows.start,
|
|
_p.s->viewportCellCount.x,
|
|
_api.invalidatedRows.end,
|
|
};
|
|
|
|
_p.dirtyRectInPx = {
|
|
til::CoordTypeMax,
|
|
til::CoordTypeMax,
|
|
til::CoordTypeMin,
|
|
til::CoordTypeMin,
|
|
};
|
|
_p.invalidatedRows = _api.invalidatedRows;
|
|
_p.cursorRect = {};
|
|
_p.scrollOffset = _api.scrollOffset;
|
|
|
|
// This if condition serves 2 purposes:
|
|
// * By setting top/bottom to the full height we ensure that we call Present() without
|
|
// any dirty rects and not Present1() on the first frame after the settings change.
|
|
// * If the scrollOffset is so large that it scrolls the entire viewport, invalidatedRows will span
|
|
// the entire viewport as well. We need to set scrollOffset to 0 then, not just because scrolling
|
|
// the contents of the entire swap chain is redundant, but more importantly because the scroll rect
|
|
// is the subset of the contents that are being scrolled into. If you scroll the entire viewport
|
|
// then the scroll rect is empty, which Present1() will loudly complain about.
|
|
if (_p.invalidatedRows == range<u16>{ 0, _p.s->viewportCellCount.y })
|
|
{
|
|
_p.MarkAllAsDirty();
|
|
}
|
|
|
|
if (const auto offset = _p.scrollOffset)
|
|
{
|
|
if (offset < 0)
|
|
{
|
|
// scrollOffset/offset = -1
|
|
// +----------+ +----------+
|
|
// | | | xxxxxxxxx|
|
|
// | xxxxxxxxx| -> |xxxxxxx |
|
|
// |xxxxxxx | | |
|
|
// +----------+ +----------+
|
|
const auto dst = std::copy_n(_p.rows.begin() - offset, _p.rows.size() + offset, _p.rowsScratch.begin());
|
|
std::copy_n(_p.rows.begin(), -offset, dst);
|
|
}
|
|
else
|
|
{
|
|
// scrollOffset/offset = 1
|
|
// +----------+ +----------+
|
|
// | xxxxxxxxx| | |
|
|
// |xxxxxxx | -> | xxxxxxxxx|
|
|
// | | |xxxxxxx |
|
|
// +----------+ +----------+
|
|
const auto dst = std::copy_n(_p.rows.end() - offset, offset, _p.rowsScratch.begin());
|
|
std::copy_n(_p.rows.begin(), _p.rows.size() - offset, dst);
|
|
}
|
|
|
|
std::swap(_p.rows, _p.rowsScratch);
|
|
|
|
// Now that the rows have scrolled, their cached dirty rects, naturally also need to do the same.
|
|
// It doesn't really matter that some of these will end up being out of bounds,
|
|
// because we'll call ShapedRow::Clear() later on which resets them.
|
|
{
|
|
const auto deltaPx = offset * _p.s->font->cellSize.y;
|
|
for (const auto r : _p.rows)
|
|
{
|
|
r->dirtyTop += deltaPx;
|
|
r->dirtyBottom += deltaPx;
|
|
}
|
|
}
|
|
|
|
// Scrolling the background bitmap is a lot easier because we can rely on memmove which works
|
|
// with both forwards and backwards copying. It's a mystery why the STL doesn't have this.
|
|
{
|
|
const auto srcOffset = std::max<ptrdiff_t>(0, -offset) * gsl::narrow_cast<ptrdiff_t>(_p.colorBitmapRowStride);
|
|
const auto dstOffset = std::max<ptrdiff_t>(0, offset) * gsl::narrow_cast<ptrdiff_t>(_p.colorBitmapRowStride);
|
|
const auto count = _p.colorBitmapDepthStride - std::max(srcOffset, dstOffset);
|
|
assert(dstOffset >= 0 && dstOffset + count <= _p.colorBitmapDepthStride);
|
|
assert(srcOffset >= 0 && srcOffset + count <= _p.colorBitmapDepthStride);
|
|
|
|
auto src = _p.colorBitmap.data() + srcOffset;
|
|
auto dst = _p.colorBitmap.data() + dstOffset;
|
|
const auto bytes = count * sizeof(u32);
|
|
|
|
for (size_t i = 0; i < 2; ++i)
|
|
{
|
|
// Avoid bumping the colorBitmapGeneration unless necessary. This approx. further halves
|
|
// the (already small) GPU load. This could easily be replaced with some custom SIMD
|
|
// to avoid going over the memory twice, but... that's a story for another day.
|
|
if (memcmp(dst, src, bytes) != 0)
|
|
{
|
|
memmove(dst, src, bytes);
|
|
_p.colorBitmapGenerations[i].bump();
|
|
}
|
|
|
|
src += _p.colorBitmapDepthStride;
|
|
dst += _p.colorBitmapDepthStride;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This serves two purposes. For each invalidated row, this will:
|
|
// * Get the old dirty rect and mark that region as needing invalidation during the upcoming Present1(),
|
|
// because it'll now be replaced with something else (for instance nothing/whitespace).
|
|
// * Clear() them to prepare them for the new incoming content from the TextBuffer.
|
|
if (_p.invalidatedRows.non_empty())
|
|
{
|
|
const til::CoordType targetSizeX = _p.s->targetSize.x;
|
|
const til::CoordType targetSizeY = _p.s->targetSize.y;
|
|
|
|
_p.dirtyRectInPx.left = 0;
|
|
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, _p.invalidatedRows.start * _p.s->font->cellSize.y);
|
|
_p.dirtyRectInPx.right = targetSizeX;
|
|
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, _p.invalidatedRows.end * _p.s->font->cellSize.y);
|
|
|
|
for (auto y = _p.invalidatedRows.start; y < _p.invalidatedRows.end; ++y)
|
|
{
|
|
const auto r = _p.rows[y];
|
|
const auto clampedTop = clamp(r->dirtyTop, 0, targetSizeY);
|
|
const auto clampedBottom = clamp(r->dirtyBottom, 0, targetSizeY);
|
|
|
|
if (clampedTop != clampedBottom)
|
|
{
|
|
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, clampedTop);
|
|
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, clampedBottom);
|
|
}
|
|
|
|
r->Clear(y, _p.s->font->cellSize.y);
|
|
}
|
|
}
|
|
|
|
#if ATLAS_DEBUG_CONTINUOUS_REDRAW
|
|
_p.MarkAllAsDirty();
|
|
#endif
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::EndPaint() noexcept
|
|
try
|
|
{
|
|
_flushBufferLine();
|
|
|
|
// PaintCursor() is only called when the cursor is visible, but we need to invalidate the cursor area
|
|
// even if it isn't. Otherwise a transition from a visible to an invisible cursor wouldn't be rendered.
|
|
if (const auto r = _api.invalidatedCursorArea; r.non_empty())
|
|
{
|
|
_p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, r.left * _p.s->font->cellSize.x);
|
|
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, r.top * _p.s->font->cellSize.y);
|
|
_p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, r.right * _p.s->font->cellSize.x);
|
|
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, r.bottom * _p.s->font->cellSize.y);
|
|
}
|
|
|
|
_api.invalidatedCursorArea = invalidatedAreaNone;
|
|
_api.invalidatedRows = invalidatedRowsNone;
|
|
_api.scrollOffset = 0;
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept
|
|
{
|
|
RETURN_HR_IF_NULL(E_INVALIDARG, pForcePaint);
|
|
*pForcePaint = false;
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::ScrollFrame() noexcept
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PrepareRenderInfo(const RenderFrameInfo& info) noexcept
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::ResetLineTransform() noexcept
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PrepareLineTransform(const LineRendition lineRendition, const til::CoordType targetRow, const til::CoordType viewportLeft) noexcept
|
|
{
|
|
const auto y = gsl::narrow_cast<u16>(clamp<til::CoordType>(targetRow, 0, _p.s->viewportCellCount.y));
|
|
_p.rows[y]->lineRendition = lineRendition;
|
|
_api.lineRendition = lineRendition;
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PaintBackground() noexcept
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PaintBufferLine(std::span<const Cluster> clusters, til::point coord, const bool fTrimLeft, const bool lineWrapped) noexcept
|
|
try
|
|
{
|
|
const auto y = gsl::narrow_cast<u16>(clamp<int>(coord.y, 0, _p.s->viewportCellCount.y));
|
|
|
|
if (_api.lastPaintBufferLineCoord.y != y)
|
|
{
|
|
_flushBufferLine();
|
|
}
|
|
|
|
const auto shift = gsl::narrow_cast<u8>(_api.lineRendition != LineRendition::SingleWidth);
|
|
const auto x = gsl::narrow_cast<u16>(clamp<int>(coord.x - (_p.s->viewportOffset.x >> shift), 0, _p.s->viewportCellCount.x));
|
|
auto columnEnd = x;
|
|
|
|
// _api.bufferLineColumn contains 1 more item than _api.bufferLine, as it represents the
|
|
// past-the-end index. It'll get appended again later once we built our new _api.bufferLine.
|
|
if (!_api.bufferLineColumn.empty())
|
|
{
|
|
_api.bufferLineColumn.pop_back();
|
|
}
|
|
|
|
// Due to the current IRenderEngine interface (that wasn't refactored yet) we need to assemble
|
|
// the current buffer line first as the remaining function operates on whole lines of text.
|
|
{
|
|
for (const auto& cluster : clusters)
|
|
{
|
|
for (const auto& ch : cluster.GetText())
|
|
{
|
|
_api.bufferLine.emplace_back(ch);
|
|
_api.bufferLineColumn.emplace_back(columnEnd);
|
|
}
|
|
columnEnd += gsl::narrow_cast<u16>(cluster.GetColumns());
|
|
}
|
|
|
|
_api.bufferLineColumn.emplace_back(columnEnd);
|
|
}
|
|
|
|
{
|
|
const auto row = _p.colorBitmap.begin() + _p.colorBitmapRowStride * y;
|
|
auto beg = row + (static_cast<size_t>(x) << shift);
|
|
auto end = row + (static_cast<size_t>(columnEnd) << shift);
|
|
|
|
const u32 colors[] = {
|
|
u32ColorPremultiply(_api.currentBackground),
|
|
_api.currentForeground,
|
|
};
|
|
|
|
for (size_t i = 0; i < 2; ++i)
|
|
{
|
|
const auto color = colors[i];
|
|
|
|
for (auto it = beg; it != end; ++it)
|
|
{
|
|
if (*it != color)
|
|
{
|
|
_p.colorBitmapGenerations[i].bump();
|
|
std::fill(it, end, color);
|
|
break;
|
|
}
|
|
}
|
|
|
|
beg += _p.colorBitmapDepthStride;
|
|
end += _p.colorBitmapDepthStride;
|
|
}
|
|
}
|
|
|
|
_api.lastPaintBufferLineCoord = { x, y };
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PaintBufferGridLines(const GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept
|
|
try
|
|
{
|
|
const auto shift = gsl::narrow_cast<u8>(_api.lineRendition != LineRendition::SingleWidth);
|
|
const auto x = std::max(0, coordTarget.x - (_p.s->viewportOffset.x >> shift));
|
|
const auto y = gsl::narrow_cast<u16>(clamp<til::CoordType>(coordTarget.y, 0, _p.s->viewportCellCount.y));
|
|
const auto from = gsl::narrow_cast<u16>(clamp<til::CoordType>(x << shift, 0, _p.s->viewportCellCount.x - 1));
|
|
const auto to = gsl::narrow_cast<u16>(clamp<size_t>((x + cchLine) << shift, from, _p.s->viewportCellCount.x));
|
|
const auto glColor = gsl::narrow_cast<u32>(gridlineColor) | 0xff000000;
|
|
const auto ulColor = gsl::narrow_cast<u32>(underlineColor) | 0xff000000;
|
|
_p.rows[y]->gridLineRanges.emplace_back(lines, glColor, ulColor, from, to);
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PaintSelection(const til::rect& rect) noexcept
|
|
try
|
|
{
|
|
// Unfortunately there's no step after Renderer::_PaintBufferOutput that
|
|
// would inform us that it's done with the last AtlasEngine::PaintBufferLine.
|
|
// As such we got to call _flushBufferLine() here just to be sure.
|
|
_flushBufferLine();
|
|
|
|
const auto y = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.top, 0, _p.s->viewportCellCount.y));
|
|
const auto from = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.left, 0, _p.s->viewportCellCount.x - 1));
|
|
const auto to = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.right, from, _p.s->viewportCellCount.x));
|
|
|
|
auto& row = *_p.rows[y];
|
|
row.selectionFrom = from;
|
|
row.selectionTo = to;
|
|
|
|
_p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, from * _p.s->font->cellSize.x);
|
|
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, y * _p.s->font->cellSize.y);
|
|
_p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, to * _p.s->font->cellSize.x);
|
|
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, _p.dirtyRectInPx.top + _p.s->font->cellSize.y);
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PaintSelections(const std::vector<til::rect>& rects) noexcept
|
|
try
|
|
{
|
|
if (rects.empty())
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
for (const auto& rect : rects)
|
|
{
|
|
const auto y = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.top, 0, _p.s->viewportCellCount.y));
|
|
const auto from = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.left, 0, _p.s->viewportCellCount.x - 1));
|
|
const auto to = gsl::narrow_cast<u16>(clamp<til::CoordType>(rect.right, from, _p.s->viewportCellCount.x));
|
|
|
|
if (rect.bottom <= 0 || rect.top >= _p.s->viewportCellCount.y)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const auto bg = &_p.backgroundBitmap[_p.colorBitmapRowStride * y];
|
|
const auto fg = &_p.foregroundBitmap[_p.colorBitmapRowStride * y];
|
|
std::fill(bg + from, bg + to, 0xff3296ff);
|
|
std::fill(fg + from, fg + to, 0xff000000);
|
|
}
|
|
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
_p.colorBitmapGenerations[i].bump();
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::PaintCursor(const CursorOptions& options) noexcept
|
|
try
|
|
{
|
|
// Unfortunately there's no step after Renderer::_PaintBufferOutput that
|
|
// would inform us that it's done with the last AtlasEngine::PaintBufferLine.
|
|
// As such we got to call _flushBufferLine() here just to be sure.
|
|
_flushBufferLine();
|
|
|
|
{
|
|
const CursorSettings cachedOptions{
|
|
.cursorColor = gsl::narrow_cast<u32>(options.fUseColor ? options.cursorColor | 0xff000000 : INVALID_COLOR),
|
|
.cursorType = gsl::narrow_cast<u16>(options.cursorType),
|
|
.heightPercentage = gsl::narrow_cast<u16>(options.ulCursorHeightPercent),
|
|
};
|
|
if (*_api.s->cursor != cachedOptions)
|
|
{
|
|
*_api.s.write()->cursor.write() = cachedOptions;
|
|
*_p.s.write()->cursor.write() = cachedOptions;
|
|
}
|
|
}
|
|
|
|
if (options.isOn)
|
|
{
|
|
const auto cursorWidth = 1 + (options.fIsDoubleWidth & (options.cursorType != CursorType::VerticalBar));
|
|
const auto top = options.coordCursor.y;
|
|
const auto bottom = top + 1;
|
|
const auto shift = gsl::narrow_cast<u8>(_p.rows[top]->lineRendition != LineRendition::SingleWidth);
|
|
auto left = options.coordCursor.x - (_p.s->viewportOffset.x >> shift);
|
|
auto right = left + cursorWidth;
|
|
|
|
left <<= shift;
|
|
right <<= shift;
|
|
|
|
_p.cursorRect = {
|
|
std::max<til::CoordType>(left, 0),
|
|
std::max<til::CoordType>(top, 0),
|
|
std::min<til::CoordType>(right, _p.s->viewportCellCount.x),
|
|
std::min<til::CoordType>(bottom, _p.s->viewportCellCount.y),
|
|
};
|
|
|
|
if (_p.cursorRect)
|
|
{
|
|
_p.dirtyRectInPx.left = std::min(_p.dirtyRectInPx.left, left * _p.s->font->cellSize.x);
|
|
_p.dirtyRectInPx.top = std::min(_p.dirtyRectInPx.top, top * _p.s->font->cellSize.y);
|
|
_p.dirtyRectInPx.right = std::max(_p.dirtyRectInPx.right, right * _p.s->font->cellSize.x);
|
|
_p.dirtyRectInPx.bottom = std::max(_p.dirtyRectInPx.bottom, bottom * _p.s->font->cellSize.y);
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
[[nodiscard]] HRESULT AtlasEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const RenderSettings& renderSettings, const gsl::not_null<IRenderData*> /*pData*/, const bool usingSoftFont, const bool isSettingDefaultBrushes) noexcept
|
|
try
|
|
{
|
|
auto [fg, bg] = renderSettings.GetAttributeColorsWithAlpha(textAttributes);
|
|
fg |= 0xff000000;
|
|
bg |= _api.backgroundOpaqueMixin;
|
|
|
|
if (!isSettingDefaultBrushes)
|
|
{
|
|
auto attributes = FontRelevantAttributes::None;
|
|
WI_SetFlagIf(attributes, FontRelevantAttributes::Bold, textAttributes.IsIntense() && renderSettings.GetRenderMode(RenderSettings::Mode::IntenseIsBold));
|
|
WI_SetFlagIf(attributes, FontRelevantAttributes::Italic, textAttributes.IsItalic());
|
|
|
|
if (_api.attributes != attributes)
|
|
{
|
|
_flushBufferLine();
|
|
}
|
|
|
|
_api.currentBackground = gsl::narrow_cast<u32>(bg);
|
|
_api.currentForeground = gsl::narrow_cast<u32>(fg);
|
|
_api.attributes = attributes;
|
|
}
|
|
else if (textAttributes.BackgroundIsDefault() && bg != _api.s->misc->backgroundColor)
|
|
{
|
|
_api.s.write()->misc.write()->backgroundColor = bg;
|
|
_p.s.write()->misc.write()->backgroundColor = bg;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
#pragma endregion
|
|
|
|
void AtlasEngine::_handleSettingsUpdate()
|
|
{
|
|
const auto targetChanged = _p.s->target != _api.s->target;
|
|
const auto fontChanged = _p.s->font != _api.s->font;
|
|
const auto cellCountChanged = _p.s->viewportCellCount != _api.s->viewportCellCount;
|
|
|
|
_p.s = _api.s;
|
|
|
|
if (targetChanged)
|
|
{
|
|
// target->useWARP affects the selection of our IDXGIAdapter which requires us to reset _p.dxgi.
|
|
// This will indirectly also recreate the backend, when AtlasEngine::_recreateAdapter() detects this change.
|
|
_p.dxgi = {};
|
|
}
|
|
if (fontChanged)
|
|
{
|
|
_recreateFontDependentResources();
|
|
}
|
|
if (cellCountChanged)
|
|
{
|
|
_recreateCellCountDependentResources();
|
|
}
|
|
|
|
_api.invalidatedRows = invalidatedRowsAll;
|
|
}
|
|
|
|
void AtlasEngine::_recreateFontDependentResources()
|
|
{
|
|
_api.replacementCharacterFontFace.reset();
|
|
_api.replacementCharacterGlyphIndex = 0;
|
|
_api.replacementCharacterLookedUp = false;
|
|
|
|
{
|
|
wchar_t localeName[LOCALE_NAME_MAX_LENGTH];
|
|
|
|
if (!GetUserDefaultLocaleName(&localeName[0], LOCALE_NAME_MAX_LENGTH))
|
|
{
|
|
memcpy(&localeName[0], L"en-US", 12);
|
|
}
|
|
|
|
_p.userLocaleName = std::wstring{ &localeName[0] };
|
|
}
|
|
|
|
if (_p.s->font->fontAxisValues.empty())
|
|
{
|
|
for (auto& axes : _api.textFormatAxes)
|
|
{
|
|
axes = {};
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// See AtlasEngine::UpdateFont.
|
|
// It hardcodes indices 0/1/2 in fontAxisValues to the weight/italic/slant axes.
|
|
// If they're -1 they haven't been set by the user and must be filled by us.
|
|
const auto& standardAxes = _p.s->font->fontAxisValues;
|
|
auto fontAxisValues = _p.s->font->fontAxisValues;
|
|
|
|
for (size_t i = 0; i < 4; ++i)
|
|
{
|
|
const auto bold = (i & static_cast<size_t>(FontRelevantAttributes::Bold)) != 0;
|
|
const auto italic = (i & static_cast<size_t>(FontRelevantAttributes::Italic)) != 0;
|
|
// The wght axis defaults to the font weight.
|
|
fontAxisValues[0].value = bold ? DWRITE_FONT_WEIGHT_BOLD : (standardAxes[0].value < 0 ? static_cast<f32>(_p.s->font->fontWeight) : standardAxes[0].value);
|
|
// The ital axis defaults to 1 if this is italic and 0 otherwise.
|
|
fontAxisValues[1].value = italic ? 1.0f : (standardAxes[1].value < 0 ? 0.0f : standardAxes[1].value);
|
|
// The slnt axis defaults to -12 if this is italic and 0 otherwise.
|
|
fontAxisValues[2].value = italic ? -12.0f : (standardAxes[2].value < 0 ? 0.0f : standardAxes[2].value);
|
|
_api.textFormatAxes[i] = { fontAxisValues.data(), fontAxisValues.size() };
|
|
}
|
|
}
|
|
|
|
_hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D;
|
|
}
|
|
|
|
void AtlasEngine::_recreateCellCountDependentResources()
|
|
{
|
|
// Let's guess that every cell consists of a surrogate pair.
|
|
const auto projectedTextSize = static_cast<size_t>(_p.s->viewportCellCount.x) * 2;
|
|
// IDWriteTextAnalyzer::GetGlyphs says:
|
|
// The recommended estimate for the per-glyph output buffers is (3 * textLength / 2 + 16).
|
|
const auto projectedGlyphSize = 3 * projectedTextSize / 2 + 16;
|
|
|
|
_api.bufferLine = std::vector<wchar_t>{};
|
|
_api.bufferLine.reserve(projectedTextSize);
|
|
_api.bufferLineColumn.reserve(projectedTextSize + 1);
|
|
|
|
_api.analysisResults = std::vector<TextAnalysisSinkResult>{};
|
|
_api.clusterMap = Buffer<u16>{ projectedTextSize };
|
|
_api.textProps = Buffer<DWRITE_SHAPING_TEXT_PROPERTIES>{ projectedTextSize };
|
|
_api.glyphIndices = Buffer<u16>{ projectedGlyphSize };
|
|
_api.glyphProps = Buffer<DWRITE_SHAPING_GLYPH_PROPERTIES>{ projectedGlyphSize };
|
|
_api.glyphAdvances = Buffer<f32>{ projectedGlyphSize };
|
|
_api.glyphOffsets = Buffer<DWRITE_GLYPH_OFFSET>{ projectedGlyphSize };
|
|
|
|
_p.unorderedRows = Buffer<ShapedRow>(_p.s->viewportCellCount.y);
|
|
_p.rowsScratch = Buffer<ShapedRow*>(_p.s->viewportCellCount.y);
|
|
_p.rows = Buffer<ShapedRow*>(_p.s->viewportCellCount.y);
|
|
|
|
// Our render loop heavily relies on memcpy() which is up to between 1.5x (Intel)
|
|
// and 40x (AMD) faster for allocations with an alignment of 32 or greater.
|
|
// backgroundBitmapStride is a "count" of u32 and not in bytes,
|
|
// so we round up to multiple of 8 because 8 * sizeof(u32) == 32.
|
|
_p.colorBitmapRowStride = alignForward<size_t>(_p.s->viewportCellCount.x, 8);
|
|
_p.colorBitmapDepthStride = _p.colorBitmapRowStride * _p.s->viewportCellCount.y;
|
|
_p.colorBitmap = Buffer<u32, 32>(_p.colorBitmapDepthStride * 2);
|
|
_p.backgroundBitmap = { _p.colorBitmap.data(), _p.colorBitmapDepthStride };
|
|
_p.foregroundBitmap = { _p.colorBitmap.data() + _p.colorBitmapDepthStride, _p.colorBitmapDepthStride };
|
|
|
|
memset(_p.colorBitmap.data(), 0, _p.colorBitmap.size() * sizeof(u32));
|
|
|
|
auto it = _p.unorderedRows.data();
|
|
for (auto& r : _p.rows)
|
|
{
|
|
r = it++;
|
|
}
|
|
}
|
|
|
|
void AtlasEngine::_flushBufferLine()
|
|
{
|
|
if (_api.bufferLine.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto cleanup = wil::scope_exit([this]() noexcept {
|
|
_api.bufferLine.clear();
|
|
_api.bufferLineColumn.clear();
|
|
});
|
|
|
|
// This would seriously blow us up otherwise.
|
|
Expects(_api.bufferLineColumn.size() == _api.bufferLine.size() + 1);
|
|
|
|
const auto beg = _api.bufferLine.data();
|
|
const auto len = _api.bufferLine.size();
|
|
size_t segmentBeg = 0;
|
|
size_t segmentEnd = 0;
|
|
bool custom = false;
|
|
|
|
if (!_hackWantsBuiltinGlyphs)
|
|
{
|
|
_mapRegularText(0, len);
|
|
return;
|
|
}
|
|
|
|
while (segmentBeg < len)
|
|
{
|
|
segmentEnd = segmentBeg;
|
|
do
|
|
{
|
|
auto i = segmentEnd;
|
|
char32_t codepoint = beg[i++];
|
|
if (til::is_leading_surrogate(codepoint) && i < len)
|
|
{
|
|
codepoint = til::combine_surrogates(codepoint, beg[i++]);
|
|
}
|
|
|
|
const auto c = BuiltinGlyphs::IsBuiltinGlyph(codepoint) || BuiltinGlyphs::IsSoftFontChar(codepoint);
|
|
if (custom != c)
|
|
{
|
|
break;
|
|
}
|
|
|
|
segmentEnd = i;
|
|
} while (segmentEnd < len);
|
|
|
|
if (segmentBeg != segmentEnd)
|
|
{
|
|
if (custom)
|
|
{
|
|
_mapBuiltinGlyphs(segmentBeg, segmentEnd);
|
|
}
|
|
else
|
|
{
|
|
_mapRegularText(segmentBeg, segmentEnd);
|
|
}
|
|
}
|
|
|
|
segmentBeg = segmentEnd;
|
|
custom = !custom;
|
|
}
|
|
}
|
|
|
|
void AtlasEngine::_mapRegularText(size_t offBeg, size_t offEnd)
|
|
{
|
|
auto& row = *_p.rows[_api.lastPaintBufferLineCoord.y];
|
|
|
|
for (u32 idx = gsl::narrow_cast<u32>(offBeg), mappedEnd = 0; idx < offEnd; idx = mappedEnd)
|
|
{
|
|
u32 mappedLength = 0;
|
|
wil::com_ptr<IDWriteFontFace2> mappedFontFace;
|
|
_mapCharacters(_api.bufferLine.data() + idx, gsl::narrow_cast<u32>(offEnd - idx), &mappedLength, mappedFontFace.addressof());
|
|
mappedEnd = idx + mappedLength;
|
|
|
|
if (!mappedFontFace)
|
|
{
|
|
_mapReplacementCharacter(idx, mappedEnd, row);
|
|
continue;
|
|
}
|
|
|
|
const auto initialIndicesCount = row.glyphIndices.size();
|
|
|
|
// GetTextComplexity() returns as many glyph indices as its textLength parameter (here: mappedLength).
|
|
// This block ensures that the buffer has sufficient capacity. It also initializes the glyphProps buffer because it and
|
|
// glyphIndices sort of form a "pair" in the _mapComplex() code and are always simultaneously resized there as well.
|
|
if (mappedLength > _api.glyphIndices.size())
|
|
{
|
|
auto size = _api.glyphIndices.size();
|
|
size = size + (size >> 1);
|
|
size = std::max<size_t>(size, mappedLength);
|
|
Expects(size > _api.glyphIndices.size());
|
|
_api.glyphIndices = Buffer<u16>{ size };
|
|
_api.glyphProps = Buffer<DWRITE_SHAPING_GLYPH_PROPERTIES>{ size };
|
|
}
|
|
|
|
if (_p.s->font->fontFeatures.empty())
|
|
{
|
|
// We can reuse idx here, as it'll be reset to "idx = mappedEnd" in the outer loop anyways.
|
|
for (u32 complexityLength = 0; idx < mappedEnd; idx += complexityLength)
|
|
{
|
|
BOOL isTextSimple = FALSE;
|
|
THROW_IF_FAILED(_p.textAnalyzer->GetTextComplexity(_api.bufferLine.data() + idx, mappedEnd - idx, mappedFontFace.get(), &isTextSimple, &complexityLength, _api.glyphIndices.data()));
|
|
|
|
if (isTextSimple)
|
|
{
|
|
const auto shift = gsl::narrow_cast<u8>(row.lineRendition != LineRendition::SingleWidth);
|
|
const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y;
|
|
|
|
for (size_t i = 0; i < complexityLength; ++i)
|
|
{
|
|
const auto col1 = _api.bufferLineColumn[idx + i + 0];
|
|
const auto col2 = _api.bufferLineColumn[idx + i + 1];
|
|
const auto glyphAdvance = (col2 - col1) * _p.s->font->cellSize.x;
|
|
const auto fg = colors[static_cast<size_t>(col1) << shift];
|
|
row.glyphIndices.emplace_back(_api.glyphIndices[i]);
|
|
row.glyphAdvances.emplace_back(static_cast<f32>(glyphAdvance));
|
|
row.glyphOffsets.emplace_back();
|
|
row.colors.emplace_back(fg);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_mapComplex(mappedFontFace.get(), idx, complexityLength, row);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_mapComplex(mappedFontFace.get(), idx, mappedLength, row);
|
|
}
|
|
|
|
const auto indicesCount = row.glyphIndices.size();
|
|
if (indicesCount > initialIndicesCount)
|
|
{
|
|
// IDWriteFontFallback::MapCharacters() isn't just awfully slow,
|
|
// it can also repeatedly return the same font face again and again. :)
|
|
if (row.mappings.empty() || row.mappings.back().fontFace != mappedFontFace)
|
|
{
|
|
row.mappings.emplace_back(std::move(mappedFontFace), gsl::narrow_cast<u32>(initialIndicesCount), gsl::narrow_cast<u32>(indicesCount));
|
|
}
|
|
else
|
|
{
|
|
row.mappings.back().glyphsTo = gsl::narrow_cast<u32>(indicesCount);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AtlasEngine::_mapBuiltinGlyphs(size_t offBeg, size_t offEnd)
|
|
{
|
|
auto& row = *_p.rows[_api.lastPaintBufferLineCoord.y];
|
|
auto initialIndicesCount = row.glyphIndices.size();
|
|
const auto shift = gsl::narrow_cast<u8>(row.lineRendition != LineRendition::SingleWidth);
|
|
const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y;
|
|
const auto base = reinterpret_cast<const u16*>(_api.bufferLine.data());
|
|
const auto len = offEnd - offBeg;
|
|
|
|
row.glyphIndices.insert(row.glyphIndices.end(), base + offBeg, base + offEnd);
|
|
row.glyphAdvances.insert(row.glyphAdvances.end(), len, static_cast<f32>(_p.s->font->cellSize.x));
|
|
row.glyphOffsets.insert(row.glyphOffsets.end(), len, {});
|
|
|
|
for (size_t i = offBeg; i < offEnd; ++i)
|
|
{
|
|
const auto col = _api.bufferLineColumn[i];
|
|
row.colors.emplace_back(colors[static_cast<size_t>(col) << shift]);
|
|
}
|
|
|
|
row.mappings.emplace_back(nullptr, gsl::narrow_cast<u32>(initialIndicesCount), gsl::narrow_cast<u32>(row.glyphIndices.size()));
|
|
}
|
|
|
|
void AtlasEngine::_mapCharacters(const wchar_t* text, const u32 textLength, u32* mappedLength, IDWriteFontFace2** mappedFontFace) const
|
|
{
|
|
TextAnalysisSource analysisSource{ _p.userLocaleName.c_str(), text, textLength };
|
|
const auto& textFormatAxis = _api.textFormatAxes[static_cast<size_t>(_api.attributes)];
|
|
|
|
// We don't read from scale anyways.
|
|
#pragma warning(suppress : 26494) // Variable 'scale' is uninitialized. Always initialize an object (type.5).
|
|
f32 scale;
|
|
|
|
if (textFormatAxis)
|
|
{
|
|
THROW_IF_FAILED(_p.s->font->fontFallback1->MapCharacters(
|
|
/* analysisSource */ &analysisSource,
|
|
/* textPosition */ 0,
|
|
/* textLength */ textLength,
|
|
/* baseFontCollection */ _p.s->font->fontCollection.get(),
|
|
/* baseFamilyName */ _p.s->font->fontName.c_str(),
|
|
/* fontAxisValues */ textFormatAxis.data(),
|
|
/* fontAxisValueCount */ gsl::narrow_cast<u32>(textFormatAxis.size()),
|
|
/* mappedLength */ mappedLength,
|
|
/* scale */ &scale,
|
|
/* mappedFontFace */ reinterpret_cast<IDWriteFontFace5**>(mappedFontFace)));
|
|
}
|
|
else
|
|
{
|
|
const auto baseWeight = WI_IsFlagSet(_api.attributes, FontRelevantAttributes::Bold) ? DWRITE_FONT_WEIGHT_BOLD : static_cast<DWRITE_FONT_WEIGHT>(_p.s->font->fontWeight);
|
|
const auto baseStyle = WI_IsFlagSet(_api.attributes, FontRelevantAttributes::Italic) ? DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL;
|
|
wil::com_ptr<IDWriteFont> font;
|
|
|
|
THROW_IF_FAILED(_p.s->font->fontFallback->MapCharacters(
|
|
/* analysisSource */ &analysisSource,
|
|
/* textPosition */ 0,
|
|
/* textLength */ textLength,
|
|
/* baseFontCollection */ _p.s->font->fontCollection.get(),
|
|
/* baseFamilyName */ _p.s->font->fontName.c_str(),
|
|
/* baseWeight */ baseWeight,
|
|
/* baseStyle */ baseStyle,
|
|
/* baseStretch */ DWRITE_FONT_STRETCH_NORMAL,
|
|
/* mappedLength */ mappedLength,
|
|
/* mappedFont */ font.addressof(),
|
|
/* scale */ &scale));
|
|
|
|
if (font)
|
|
{
|
|
THROW_IF_FAILED(font->CreateFontFace(reinterpret_cast<IDWriteFontFace**>(mappedFontFace)));
|
|
}
|
|
}
|
|
|
|
// Oh wow! You found a case where scale isn't 1! I tried every font and none
|
|
// returned something besides 1. I just couldn't figure out why this exists.
|
|
assert(scale == 1);
|
|
}
|
|
|
|
void AtlasEngine::_mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 length, ShapedRow& row)
|
|
{
|
|
_api.analysisResults.clear();
|
|
|
|
TextAnalysisSource analysisSource{ _p.userLocaleName.c_str(), _api.bufferLine.data(), gsl::narrow<UINT32>(_api.bufferLine.size()) };
|
|
TextAnalysisSink analysisSink{ _api.analysisResults };
|
|
THROW_IF_FAILED(_p.textAnalyzer->AnalyzeScript(&analysisSource, idx, length, &analysisSink));
|
|
|
|
for (const auto& a : _api.analysisResults)
|
|
{
|
|
u32 actualGlyphCount = 0;
|
|
|
|
#pragma warning(push)
|
|
#pragma warning(disable : 26494) // Variable '...' is uninitialized. Always initialize an object (type.5).
|
|
// None of these variables need to be initialized.
|
|
// features/featureRangeLengths are marked _In_reads_opt_(featureRanges).
|
|
// featureRanges is only > 0 when we also initialize all these variables.
|
|
DWRITE_TYPOGRAPHIC_FEATURES feature;
|
|
const DWRITE_TYPOGRAPHIC_FEATURES* features;
|
|
u32 featureRangeLengths;
|
|
#pragma warning(pop)
|
|
u32 featureRanges = 0;
|
|
|
|
if (!_p.s->font->fontFeatures.empty())
|
|
{
|
|
// Direct2D, why is this mutable? Why?
|
|
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3).
|
|
feature.features = const_cast<DWRITE_FONT_FEATURE*>(_p.s->font->fontFeatures.data());
|
|
feature.featureCount = gsl::narrow_cast<u32>(_p.s->font->fontFeatures.size());
|
|
features = &feature;
|
|
featureRangeLengths = a.textLength;
|
|
featureRanges = 1;
|
|
}
|
|
|
|
if (_api.clusterMap.size() <= a.textLength)
|
|
{
|
|
_api.clusterMap = Buffer<u16>{ static_cast<size_t>(a.textLength) + 1 };
|
|
_api.textProps = Buffer<DWRITE_SHAPING_TEXT_PROPERTIES>{ a.textLength };
|
|
}
|
|
|
|
for (auto retry = 0;;)
|
|
{
|
|
const auto hr = _p.textAnalyzer->GetGlyphs(
|
|
/* textString */ _api.bufferLine.data() + a.textPosition,
|
|
/* textLength */ a.textLength,
|
|
/* fontFace */ mappedFontFace,
|
|
/* isSideways */ false,
|
|
/* isRightToLeft */ 0,
|
|
/* scriptAnalysis */ &a.analysis,
|
|
/* localeName */ _p.userLocaleName.c_str(),
|
|
/* numberSubstitution */ nullptr,
|
|
/* features */ &features,
|
|
/* featureRangeLengths */ &featureRangeLengths,
|
|
/* featureRanges */ featureRanges,
|
|
/* maxGlyphCount */ gsl::narrow_cast<u32>(_api.glyphIndices.size()),
|
|
/* clusterMap */ _api.clusterMap.data(),
|
|
/* textProps */ _api.textProps.data(),
|
|
/* glyphIndices */ _api.glyphIndices.data(),
|
|
/* glyphProps */ _api.glyphProps.data(),
|
|
/* actualGlyphCount */ &actualGlyphCount);
|
|
|
|
if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) && ++retry < 8)
|
|
{
|
|
// Grow factor 1.5x.
|
|
auto size = _api.glyphIndices.size();
|
|
size = size + (size >> 1);
|
|
// Overflow check.
|
|
Expects(size > _api.glyphIndices.size());
|
|
_api.glyphIndices = Buffer<u16>{ size };
|
|
_api.glyphProps = Buffer<DWRITE_SHAPING_GLYPH_PROPERTIES>{ size };
|
|
continue;
|
|
}
|
|
|
|
THROW_IF_FAILED(hr);
|
|
break;
|
|
}
|
|
|
|
if (_api.glyphAdvances.size() < actualGlyphCount)
|
|
{
|
|
// Grow the buffer by at least 1.5x and at least of `actualGlyphCount` items.
|
|
// The 1.5x growth ensures we don't reallocate every time we need 1 more slot.
|
|
auto size = _api.glyphAdvances.size();
|
|
size = size + (size >> 1);
|
|
size = std::max<size_t>(size, actualGlyphCount);
|
|
_api.glyphAdvances = Buffer<f32>{ size };
|
|
_api.glyphOffsets = Buffer<DWRITE_GLYPH_OFFSET>{ size };
|
|
}
|
|
|
|
THROW_IF_FAILED(_p.textAnalyzer->GetGlyphPlacements(
|
|
/* textString */ _api.bufferLine.data() + a.textPosition,
|
|
/* clusterMap */ _api.clusterMap.data(),
|
|
/* textProps */ _api.textProps.data(),
|
|
/* textLength */ a.textLength,
|
|
/* glyphIndices */ _api.glyphIndices.data(),
|
|
/* glyphProps */ _api.glyphProps.data(),
|
|
/* glyphCount */ actualGlyphCount,
|
|
/* fontFace */ mappedFontFace,
|
|
/* fontEmSize */ _p.s->font->fontSize,
|
|
/* isSideways */ false,
|
|
/* isRightToLeft */ 0,
|
|
/* scriptAnalysis */ &a.analysis,
|
|
/* localeName */ _p.userLocaleName.c_str(),
|
|
/* features */ &features,
|
|
/* featureRangeLengths */ &featureRangeLengths,
|
|
/* featureRanges */ featureRanges,
|
|
/* glyphAdvances */ _api.glyphAdvances.data(),
|
|
/* glyphOffsets */ _api.glyphOffsets.data()));
|
|
|
|
_api.clusterMap[a.textLength] = gsl::narrow_cast<u16>(actualGlyphCount);
|
|
|
|
const auto shift = gsl::narrow_cast<u8>(row.lineRendition != LineRendition::SingleWidth);
|
|
const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y;
|
|
auto prevCluster = _api.clusterMap[0];
|
|
size_t beg = 0;
|
|
|
|
for (size_t i = 1; i <= a.textLength; ++i)
|
|
{
|
|
const auto nextCluster = _api.clusterMap[i];
|
|
if (prevCluster == nextCluster)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const size_t col1 = _api.bufferLineColumn[a.textPosition + beg];
|
|
const size_t col2 = _api.bufferLineColumn[a.textPosition + i];
|
|
const auto fg = colors[col1 << shift];
|
|
|
|
const auto expectedAdvance = (col2 - col1) * _p.s->font->cellSize.x;
|
|
f32 actualAdvance = 0;
|
|
for (auto j = prevCluster; j < nextCluster; ++j)
|
|
{
|
|
actualAdvance += _api.glyphAdvances[j];
|
|
}
|
|
_api.glyphAdvances[nextCluster - 1] += expectedAdvance - actualAdvance;
|
|
|
|
row.colors.insert(row.colors.end(), nextCluster - prevCluster, fg);
|
|
|
|
prevCluster = nextCluster;
|
|
beg = i;
|
|
}
|
|
|
|
row.glyphIndices.insert(row.glyphIndices.end(), _api.glyphIndices.begin(), _api.glyphIndices.begin() + actualGlyphCount);
|
|
row.glyphAdvances.insert(row.glyphAdvances.end(), _api.glyphAdvances.begin(), _api.glyphAdvances.begin() + actualGlyphCount);
|
|
row.glyphOffsets.insert(row.glyphOffsets.end(), _api.glyphOffsets.begin(), _api.glyphOffsets.begin() + actualGlyphCount);
|
|
}
|
|
}
|
|
|
|
void AtlasEngine::_mapReplacementCharacter(u32 from, u32 to, ShapedRow& row)
|
|
{
|
|
if (!_api.replacementCharacterLookedUp)
|
|
{
|
|
bool succeeded = false;
|
|
|
|
u32 mappedLength = 0;
|
|
_mapCharacters(L"\uFFFD", 1, &mappedLength, _api.replacementCharacterFontFace.put());
|
|
|
|
if (mappedLength == 1)
|
|
{
|
|
static constexpr u32 codepoint = 0xFFFD;
|
|
succeeded = SUCCEEDED(_api.replacementCharacterFontFace->GetGlyphIndicesW(&codepoint, 1, &_api.replacementCharacterGlyphIndex));
|
|
}
|
|
|
|
if (!succeeded)
|
|
{
|
|
_api.replacementCharacterFontFace.reset();
|
|
_api.replacementCharacterGlyphIndex = 0;
|
|
}
|
|
|
|
_api.replacementCharacterLookedUp = true;
|
|
}
|
|
|
|
if (!_api.replacementCharacterFontFace)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto pos = from;
|
|
auto col1 = _api.bufferLineColumn[from];
|
|
auto initialIndicesCount = row.glyphIndices.size();
|
|
const auto shift = gsl::narrow_cast<u8>(row.lineRendition != LineRendition::SingleWidth);
|
|
const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y;
|
|
|
|
while (pos < to)
|
|
{
|
|
const auto col2 = _api.bufferLineColumn[++pos];
|
|
if (col1 == col2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
row.glyphIndices.emplace_back(_api.replacementCharacterGlyphIndex);
|
|
row.glyphAdvances.emplace_back(static_cast<f32>((col2 - col1) * _p.s->font->cellSize.x));
|
|
row.glyphOffsets.emplace_back();
|
|
row.colors.emplace_back(colors[static_cast<size_t>(col1) << shift]);
|
|
|
|
col1 = col2;
|
|
}
|
|
|
|
{
|
|
const auto indicesCount = row.glyphIndices.size();
|
|
const auto fontFace = _api.replacementCharacterFontFace.get();
|
|
|
|
if (indicesCount > initialIndicesCount)
|
|
{
|
|
row.mappings.emplace_back(fontFace, gsl::narrow_cast<u32>(initialIndicesCount), gsl::narrow_cast<u32>(indicesCount));
|
|
}
|
|
}
|
|
}
|