mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-12 00:07:24 -06:00
This commit fixes a number of issues around horizontal scrolling. DxEngine only had one bug, where the clip rect would cause any content outside of the actual viewport to be invisible. AtlasEngine had more bugs, mostly around the conversion from textbuffer-relative coordinates to viewport-relative coordinates, since AtlasEngine stores things like the cursor position, attributes, etc., relative to the viewport. It also renames `cellCount` to `viewportCellCount`, because I realized that it might have to deal with a `textBufferCellCount` or similar in the future. I hope that the new name is more descriptive of what it refers to. Future improvements to AtlasEngine in particular would be to not copy the entire `Settings` struct every time the horizontal scroll offset changes, and to trim trailing whitespace before shaping text. This is in preparation for #1860 ## Validation Steps Performed * Patch `RenderingTests` to run in the main (and not alt) buffer * Horizontal scrolling of line renditions and attributes works ✅ * Selection retains its position (mostly) ✅
688 lines
26 KiB
C++
688 lines
26 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "pch.h"
|
|
#include "BackendD2D.h"
|
|
|
|
#if ATLAS_DEBUG_SHOW_DIRTY
|
|
#include "colorbrewer.h"
|
|
#endif
|
|
|
|
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
|
|
#include "wic.h"
|
|
#endif
|
|
|
|
TIL_FAST_MATH_BEGIN
|
|
|
|
// 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 : 26482) // Only index into arrays using constant expressions (bounds.2).
|
|
|
|
using namespace Microsoft::Console::Render::Atlas;
|
|
|
|
void BackendD2D::ReleaseResources() noexcept
|
|
{
|
|
_renderTarget.reset();
|
|
_renderTarget4.reset();
|
|
// Ensure _handleSettingsUpdate() is called so that _renderTarget gets recreated.
|
|
_generation = {};
|
|
}
|
|
|
|
void BackendD2D::Render(RenderingPayload& p)
|
|
{
|
|
if (_generation != p.s.generation())
|
|
{
|
|
_handleSettingsUpdate(p);
|
|
}
|
|
|
|
_renderTarget->BeginDraw();
|
|
#if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_DUMP_RENDER_TARGET
|
|
// Invalidating the render target helps with spotting Present1() bugs.
|
|
_renderTarget->Clear();
|
|
#endif
|
|
_drawBackground(p);
|
|
_drawCursorPart1(p);
|
|
_drawText(p);
|
|
_drawCursorPart2(p);
|
|
_drawSelection(p);
|
|
#if ATLAS_DEBUG_SHOW_DIRTY
|
|
_debugShowDirty(p);
|
|
#endif
|
|
THROW_IF_FAILED(_renderTarget->EndDraw());
|
|
|
|
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
|
|
_debugDumpRenderTarget(p);
|
|
#endif
|
|
}
|
|
|
|
bool BackendD2D::RequiresContinuousRedraw() noexcept
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p)
|
|
{
|
|
const auto renderTargetChanged = !_renderTarget;
|
|
const auto fontChanged = _fontGeneration != p.s->font.generation();
|
|
const auto cursorChanged = _cursorGeneration != p.s->cursor.generation();
|
|
const auto cellCountChanged = _viewportCellCount != p.s->viewportCellCount;
|
|
|
|
if (renderTargetChanged)
|
|
{
|
|
{
|
|
wil::com_ptr<ID3D11Texture2D> buffer;
|
|
THROW_IF_FAILED(p.swapChain.swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(buffer.addressof())));
|
|
const auto surface = buffer.query<IDXGISurface>();
|
|
|
|
const D2D1_RENDER_TARGET_PROPERTIES props{
|
|
.type = D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
|
.pixelFormat = { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED },
|
|
.dpiX = static_cast<f32>(p.s->font->dpi),
|
|
.dpiY = static_cast<f32>(p.s->font->dpi),
|
|
};
|
|
// ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not.
|
|
THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast<ID2D1RenderTarget**>(_renderTarget.addressof())));
|
|
_renderTarget.try_query_to(_renderTarget4.addressof());
|
|
|
|
_renderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS);
|
|
_renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
|
|
}
|
|
{
|
|
static constexpr D2D1_COLOR_F color{};
|
|
THROW_IF_FAILED(_renderTarget->CreateSolidColorBrush(&color, nullptr, _emojiBrush.put()));
|
|
THROW_IF_FAILED(_renderTarget->CreateSolidColorBrush(&color, nullptr, _brush.put()));
|
|
_brushColor = 0;
|
|
}
|
|
}
|
|
|
|
if (!_dottedStrokeStyle)
|
|
{
|
|
static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM };
|
|
static constexpr FLOAT dashes[2]{ 1, 1 };
|
|
THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof()));
|
|
}
|
|
|
|
if (renderTargetChanged || fontChanged)
|
|
{
|
|
const auto dpi = static_cast<f32>(p.s->font->dpi);
|
|
_renderTarget->SetDpi(dpi, dpi);
|
|
_renderTarget->SetTextAntialiasMode(static_cast<D2D1_TEXT_ANTIALIAS_MODE>(p.s->font->antialiasingMode));
|
|
}
|
|
|
|
if (renderTargetChanged || fontChanged || cellCountChanged)
|
|
{
|
|
const D2D1_BITMAP_PROPERTIES props{
|
|
.pixelFormat = { DXGI_FORMAT_R8G8B8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED },
|
|
.dpiX = static_cast<f32>(p.s->font->dpi),
|
|
.dpiY = static_cast<f32>(p.s->font->dpi),
|
|
};
|
|
const D2D1_SIZE_U size{
|
|
p.s->viewportCellCount.x,
|
|
p.s->viewportCellCount.y,
|
|
};
|
|
const D2D1_MATRIX_3X2_F transform{
|
|
.m11 = static_cast<f32>(p.s->font->cellSize.x),
|
|
.m22 = static_cast<f32>(p.s->font->cellSize.y),
|
|
};
|
|
THROW_IF_FAILED(_renderTarget->CreateBitmap(size, nullptr, 0, &props, _backgroundBitmap.put()));
|
|
THROW_IF_FAILED(_renderTarget->CreateBitmapBrush(_backgroundBitmap.get(), _backgroundBrush.put()));
|
|
_backgroundBrush->SetInterpolationMode(D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR);
|
|
_backgroundBrush->SetExtendModeX(D2D1_EXTEND_MODE_MIRROR);
|
|
_backgroundBrush->SetExtendModeY(D2D1_EXTEND_MODE_MIRROR);
|
|
_backgroundBrush->SetTransform(&transform);
|
|
_backgroundBitmapGeneration = {};
|
|
}
|
|
|
|
if (fontChanged || cursorChanged)
|
|
{
|
|
_cursorBitmap.reset();
|
|
_cursorBitmapSize = {};
|
|
}
|
|
|
|
_generation = p.s.generation();
|
|
_fontGeneration = p.s->font.generation();
|
|
_cursorGeneration = p.s->cursor.generation();
|
|
_viewportCellCount = p.s->viewportCellCount;
|
|
}
|
|
|
|
void BackendD2D::_drawBackground(const RenderingPayload& p) noexcept
|
|
{
|
|
if (_backgroundBitmapGeneration != p.colorBitmapGenerations[0])
|
|
{
|
|
_backgroundBitmap->CopyFromMemory(nullptr, p.backgroundBitmap.data(), gsl::narrow_cast<UINT32>(p.colorBitmapRowStride * sizeof(u32)));
|
|
_backgroundBitmapGeneration = p.colorBitmapGenerations[0];
|
|
}
|
|
|
|
// If the terminal was 120x30 cells and 1200x600 pixels large, this would draw the
|
|
// background by upscaling a 120x30 pixel bitmap to fill the entire render target.
|
|
const D2D1_RECT_F rect{ 0, 0, static_cast<f32>(p.s->targetSize.x), static_cast<f32>(p.s->targetSize.y) };
|
|
_renderTarget->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_COPY);
|
|
_renderTarget->FillRectangle(&rect, _backgroundBrush.get());
|
|
_renderTarget->SetPrimitiveBlend(D2D1_PRIMITIVE_BLEND_SOURCE_OVER);
|
|
}
|
|
|
|
void BackendD2D::_drawText(RenderingPayload& p)
|
|
{
|
|
til::CoordType dirtyTop = til::CoordTypeMax;
|
|
til::CoordType dirtyBottom = til::CoordTypeMin;
|
|
|
|
// It is possible to create a "_foregroundBrush" similar to how the `_backgroundBrush` is created and
|
|
// use that as the brush for text rendering below. That way we wouldn't have to search `row->colors` for color
|
|
// changes and could draw entire lines of text in a single call. Unfortunately Direct2D is not particularly
|
|
// smart if you do this and chooses to draw the given text into a way too small offscreen texture first and
|
|
// then blends it on the screen with the given bitmap brush. While this roughly doubles the performance
|
|
// when drawing lots of colors, the extra latency drops performance by >10x when drawing fewer colors.
|
|
// Since fewer colors are more common, I've chosen to go with regular solid-color brushes.
|
|
u16 y = 0;
|
|
for (const auto row : p.rows)
|
|
{
|
|
auto baselineX = 0.0f;
|
|
auto baselineY = static_cast<f32>(p.s->font->cellSize.y * y + p.s->font->baseline);
|
|
|
|
if (row->lineRendition != LineRendition::SingleWidth)
|
|
{
|
|
baselineY = _drawTextPrepareLineRendition(p, row, baselineY);
|
|
}
|
|
|
|
for (const auto& m : row->mappings)
|
|
{
|
|
const auto colorsBegin = row->colors.begin();
|
|
auto it = colorsBegin + m.glyphsFrom;
|
|
const auto end = colorsBegin + m.glyphsTo;
|
|
|
|
while (it != end)
|
|
{
|
|
const auto beg = it;
|
|
const auto off = it - colorsBegin;
|
|
const auto fg = *it;
|
|
|
|
while (++it != end && *it == fg)
|
|
{
|
|
}
|
|
|
|
const auto count = it - beg;
|
|
const auto brush = _brushWithColor(fg);
|
|
const DWRITE_GLYPH_RUN glyphRun{
|
|
.fontFace = m.fontFace.get(),
|
|
.fontEmSize = p.s->font->fontSize,
|
|
.glyphCount = gsl::narrow_cast<UINT32>(count),
|
|
.glyphIndices = &row->glyphIndices[off],
|
|
.glyphAdvances = &row->glyphAdvances[off],
|
|
.glyphOffsets = &row->glyphOffsets[off],
|
|
};
|
|
const D2D1_POINT_2F baselineOrigin{
|
|
baselineX,
|
|
baselineY,
|
|
};
|
|
|
|
if (glyphRun.fontFace)
|
|
{
|
|
D2D1_RECT_F bounds = GlyphRunEmptyBounds;
|
|
|
|
if (const auto enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun))
|
|
{
|
|
while (ColorGlyphRunMoveNext(enumerator.get()))
|
|
{
|
|
const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get());
|
|
ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun);
|
|
ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL);
|
|
GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds);
|
|
}
|
|
|
|
if (bounds.top < bounds.bottom)
|
|
{
|
|
// Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding.
|
|
if (row->lineRendition != LineRendition::DoubleHeightTop)
|
|
{
|
|
row->dirtyBottom = std::max(row->dirtyBottom, static_cast<i32>(lrintf(bounds.bottom)));
|
|
}
|
|
if (row->lineRendition != LineRendition::DoubleHeightBottom)
|
|
{
|
|
row->dirtyTop = std::min(row->dirtyTop, static_cast<i32>(lrintf(bounds.top)));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (UINT32 i = 0; i < glyphRun.glyphCount; ++i)
|
|
{
|
|
baselineX += glyphRun.glyphAdvances[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!row->gridLineRanges.empty())
|
|
{
|
|
_drawGridlineRow(p, row, y);
|
|
}
|
|
|
|
if (row->lineRendition != LineRendition::SingleWidth)
|
|
{
|
|
_drawTextResetLineRendition(row);
|
|
}
|
|
|
|
if (p.invalidatedRows.contains(y))
|
|
{
|
|
dirtyTop = std::min(dirtyTop, row->dirtyTop);
|
|
dirtyBottom = std::max(dirtyBottom, row->dirtyBottom);
|
|
}
|
|
|
|
++y;
|
|
}
|
|
|
|
if (dirtyTop < dirtyBottom)
|
|
{
|
|
p.dirtyRectInPx.top = std::min(p.dirtyRectInPx.top, dirtyTop);
|
|
p.dirtyRectInPx.bottom = std::max(p.dirtyRectInPx.bottom, dirtyBottom);
|
|
}
|
|
}
|
|
|
|
f32 BackendD2D::_drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept
|
|
{
|
|
const auto lineRendition = row->lineRendition;
|
|
D2D1_MATRIX_3X2_F transform{
|
|
.m11 = 2.0f,
|
|
.m22 = 1.0f,
|
|
};
|
|
|
|
if (lineRendition >= LineRendition::DoubleHeightTop)
|
|
{
|
|
D2D1_RECT_F clipRect{ 0, 0, static_cast<f32>(p.s->targetSize.x), static_cast<f32>(p.s->targetSize.y) };
|
|
|
|
transform.m22 = 2.0f;
|
|
transform.dy = -1.0f * (baselineY + p.s->font->descender);
|
|
|
|
// If you print the top half of a double height row (DECDHL), the expectation is that only
|
|
// the top half is visible, which requires us to keep the clip rect at the bottom of the row.
|
|
// (Vice versa for the bottom half of a double height row.)
|
|
if (lineRendition == LineRendition::DoubleHeightTop)
|
|
{
|
|
const auto delta = static_cast<f32>(p.s->font->cellSize.y);
|
|
baselineY += delta;
|
|
transform.dy -= delta;
|
|
clipRect.bottom = static_cast<f32>(row->dirtyBottom);
|
|
}
|
|
else
|
|
{
|
|
clipRect.top = static_cast<f32>(row->dirtyTop);
|
|
}
|
|
|
|
_renderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
|
|
}
|
|
|
|
_renderTarget->SetTransform(&transform);
|
|
return baselineY;
|
|
}
|
|
|
|
void BackendD2D::_drawTextResetLineRendition(const ShapedRow* row) const noexcept
|
|
{
|
|
static constexpr D2D1_MATRIX_3X2_F identity{ .m11 = 1, .m22 = 1 };
|
|
_renderTarget->SetTransform(&identity);
|
|
|
|
if (row->lineRendition >= LineRendition::DoubleHeightTop)
|
|
{
|
|
_renderTarget->PopAxisAlignedClip();
|
|
}
|
|
}
|
|
|
|
// Returns the theoretical/design design size of the given `DWRITE_GLYPH_RUN`, relative the the given baseline origin.
|
|
// This algorithm replicates what DirectWrite does internally to provide `IDWriteTextLayout::GetMetrics`.
|
|
f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY)
|
|
{
|
|
DWRITE_FONT_METRICS fontMetrics;
|
|
glyphRun.fontFace->GetMetrics(&fontMetrics);
|
|
|
|
if (glyphRun.glyphCount > _glyphMetrics.size())
|
|
{
|
|
// Growth factor 1.5x.
|
|
auto size = _glyphMetrics.size();
|
|
size = size + (size >> 1);
|
|
size = std::max<size_t>(size, glyphRun.glyphCount);
|
|
// Overflow check.
|
|
Expects(size > _glyphMetrics.size());
|
|
_glyphMetrics = Buffer<DWRITE_GLYPH_METRICS>{ size };
|
|
}
|
|
|
|
glyphRun.fontFace->GetDesignGlyphMetrics(glyphRun.glyphIndices, glyphRun.glyphCount, _glyphMetrics.data(), false);
|
|
|
|
const f32 fontScale = glyphRun.fontEmSize / fontMetrics.designUnitsPerEm;
|
|
f32r accumulatedBounds{
|
|
baselineX,
|
|
baselineY,
|
|
baselineX,
|
|
baselineY,
|
|
};
|
|
|
|
for (uint32_t i = 0; i < glyphRun.glyphCount; ++i)
|
|
{
|
|
const auto& glyphMetrics = _glyphMetrics[i];
|
|
const auto glyphAdvance = glyphRun.glyphAdvances ? glyphRun.glyphAdvances[i] : glyphMetrics.advanceWidth * fontScale;
|
|
|
|
const auto left = static_cast<f32>(glyphMetrics.leftSideBearing) * fontScale;
|
|
const auto top = static_cast<f32>(glyphMetrics.topSideBearing - glyphMetrics.verticalOriginY) * fontScale;
|
|
const auto right = static_cast<f32>(gsl::narrow_cast<INT32>(glyphMetrics.advanceWidth) - glyphMetrics.rightSideBearing) * fontScale;
|
|
const auto bottom = static_cast<f32>(gsl::narrow_cast<INT32>(glyphMetrics.advanceHeight) - glyphMetrics.bottomSideBearing - glyphMetrics.verticalOriginY) * fontScale;
|
|
|
|
if (left < right && top < bottom)
|
|
{
|
|
auto glyphX = baselineX;
|
|
auto glyphY = baselineY;
|
|
if (glyphRun.glyphOffsets)
|
|
{
|
|
glyphX += glyphRun.glyphOffsets[i].advanceOffset;
|
|
glyphY -= glyphRun.glyphOffsets[i].ascenderOffset;
|
|
}
|
|
|
|
accumulatedBounds.left = std::min(accumulatedBounds.left, left + glyphX);
|
|
accumulatedBounds.top = std::min(accumulatedBounds.top, top + glyphY);
|
|
accumulatedBounds.right = std::max(accumulatedBounds.right, right + glyphX);
|
|
accumulatedBounds.bottom = std::max(accumulatedBounds.bottom, bottom + glyphY);
|
|
}
|
|
|
|
baselineX += glyphAdvance;
|
|
}
|
|
|
|
return accumulatedBounds;
|
|
}
|
|
|
|
void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y)
|
|
{
|
|
const auto widthShift = gsl::narrow_cast<u8>(row->lineRendition != LineRendition::SingleWidth);
|
|
const auto cellSize = p.s->font->cellSize;
|
|
const auto rowTop = gsl::narrow_cast<i16>(cellSize.y * y);
|
|
const auto rowBottom = gsl::narrow_cast<i16>(rowTop + cellSize.y);
|
|
const auto textCellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop;
|
|
|
|
const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) {
|
|
const auto from = r.from >> widthShift;
|
|
const auto to = r.to >> widthShift;
|
|
|
|
auto posX = from * cellSize.x + pos.position;
|
|
const auto end = to * cellSize.x;
|
|
|
|
D2D1_POINT_2F point0{ 0, static_cast<f32>(textCellCenter) };
|
|
D2D1_POINT_2F point1{ 0, static_cast<f32>(textCellCenter + cellSize.y) };
|
|
const auto brush = _brushWithColor(r.color);
|
|
const f32 w = pos.height;
|
|
const f32 hw = w * 0.5f;
|
|
|
|
for (; posX < end; posX += cellSize.x)
|
|
{
|
|
const auto centerX = posX + hw;
|
|
point0.x = centerX;
|
|
point1.x = centerX;
|
|
_renderTarget->DrawLine(point0, point1, brush, w, nullptr);
|
|
}
|
|
};
|
|
const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle) {
|
|
const auto from = r.from >> widthShift;
|
|
const auto to = r.to >> widthShift;
|
|
|
|
const auto brush = _brushWithColor(r.color);
|
|
const f32 w = pos.height;
|
|
const f32 centerY = textCellCenter + pos.position + w * 0.5f;
|
|
const D2D1_POINT_2F point0{ static_cast<f32>(from * cellSize.x), centerY };
|
|
const D2D1_POINT_2F point1{ static_cast<f32>(to * cellSize.x), centerY };
|
|
_renderTarget->DrawLine(point0, point1, brush, w, strokeStyle);
|
|
};
|
|
|
|
for (const auto& r : row->gridLineRanges)
|
|
{
|
|
// AtlasEngine.cpp shouldn't add any gridlines if they don't do anything.
|
|
assert(r.lines.any());
|
|
|
|
if (r.lines.test(GridLines::Left))
|
|
{
|
|
appendVerticalLines(r, p.s->font->gridLeft);
|
|
}
|
|
if (r.lines.test(GridLines::Right))
|
|
{
|
|
appendVerticalLines(r, p.s->font->gridRight);
|
|
}
|
|
if (r.lines.test(GridLines::Top))
|
|
{
|
|
appendHorizontalLine(r, p.s->font->gridTop, nullptr);
|
|
}
|
|
if (r.lines.test(GridLines::Bottom))
|
|
{
|
|
appendHorizontalLine(r, p.s->font->gridBottom, nullptr);
|
|
}
|
|
|
|
if (r.lines.test(GridLines::Underline))
|
|
{
|
|
appendHorizontalLine(r, p.s->font->underline, nullptr);
|
|
}
|
|
if (r.lines.test(GridLines::HyperlinkUnderline))
|
|
{
|
|
appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get());
|
|
}
|
|
if (r.lines.test(GridLines::DoubleUnderline))
|
|
{
|
|
for (const auto pos : p.s->font->doubleUnderline)
|
|
{
|
|
appendHorizontalLine(r, pos, nullptr);
|
|
}
|
|
}
|
|
if (r.lines.test(GridLines::Strikethrough))
|
|
{
|
|
appendHorizontalLine(r, p.s->font->strikethrough, nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BackendD2D::_drawCursorPart1(const RenderingPayload& p)
|
|
{
|
|
if (p.cursorRect.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto cursorColor = p.s->cursor->cursorColor;
|
|
|
|
if (cursorColor != 0xffffffff)
|
|
{
|
|
const D2D1_RECT_F rect{
|
|
static_cast<f32>(p.cursorRect.left * p.s->font->cellSize.x),
|
|
static_cast<f32>(p.cursorRect.top * p.s->font->cellSize.y),
|
|
static_cast<f32>(p.cursorRect.right * p.s->font->cellSize.x),
|
|
static_cast<f32>(p.cursorRect.bottom * p.s->font->cellSize.y),
|
|
};
|
|
const auto brush = _brushWithColor(cursorColor);
|
|
_drawCursor(p, _renderTarget.get(), rect, brush);
|
|
}
|
|
}
|
|
|
|
void BackendD2D::_drawCursorPart2(const RenderingPayload& p)
|
|
{
|
|
if (p.cursorRect.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (p.s->cursor->cursorColor == 0xffffffff)
|
|
{
|
|
const auto cursorSize = p.cursorRect.size();
|
|
if (cursorSize != _cursorBitmapSize)
|
|
{
|
|
_resizeCursorBitmap(p, cursorSize);
|
|
}
|
|
|
|
const D2D1_POINT_2F target{
|
|
static_cast<f32>(p.cursorRect.left * p.s->font->cellSize.x),
|
|
static_cast<f32>(p.cursorRect.top * p.s->font->cellSize.y),
|
|
};
|
|
_renderTarget->DrawImage(_cursorBitmap.get(), &target, nullptr, D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR, D2D1_COMPOSITE_MODE_MASK_INVERT);
|
|
}
|
|
}
|
|
|
|
void BackendD2D::_resizeCursorBitmap(const RenderingPayload& p, const til::size newSize)
|
|
{
|
|
const til::size newSizeInPx{
|
|
newSize.width * p.s->font->cellSize.x,
|
|
newSize.height * p.s->font->cellSize.y,
|
|
};
|
|
|
|
// CreateCompatibleRenderTarget is a terrific API and does not adopt _any_ of the settings of the
|
|
// parent render target (like the AA mode or D2D1_UNIT_MODE_PIXELS). Not sure who came up with that,
|
|
// but fact is that we need to set both sizes to override the DPI and fake D2D1_UNIT_MODE_PIXELS.
|
|
const D2D1_SIZE_F sizeF{ static_cast<f32>(newSizeInPx.width), static_cast<f32>(newSizeInPx.height) };
|
|
const D2D1_SIZE_U sizeU{ gsl::narrow_cast<UINT32>(newSizeInPx.width), gsl::narrow_cast<UINT32>(newSizeInPx.height) };
|
|
wil::com_ptr<ID2D1BitmapRenderTarget> cursorRenderTarget;
|
|
_renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, nullptr, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, cursorRenderTarget.addressof());
|
|
cursorRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
|
|
|
|
cursorRenderTarget->BeginDraw();
|
|
cursorRenderTarget->Clear();
|
|
{
|
|
const D2D1_RECT_F rect{ 0, 0, sizeF.width, sizeF.height };
|
|
const auto brush = _brushWithColor(0xffffffff);
|
|
_drawCursor(p, cursorRenderTarget.get(), rect, brush);
|
|
}
|
|
THROW_IF_FAILED(cursorRenderTarget->EndDraw());
|
|
|
|
cursorRenderTarget->GetBitmap(_cursorBitmap.put());
|
|
_cursorBitmapSize = newSize;
|
|
}
|
|
|
|
void BackendD2D::_drawCursor(const RenderingPayload& p, ID2D1RenderTarget* renderTarget, D2D1_RECT_F rect, ID2D1Brush* brush) noexcept
|
|
{
|
|
switch (static_cast<CursorType>(p.s->cursor->cursorType))
|
|
{
|
|
case CursorType::Legacy:
|
|
{
|
|
const auto height = p.s->cursor->heightPercentage / 100.0f;
|
|
rect.top = roundf((rect.top - rect.bottom) * height + rect.bottom);
|
|
renderTarget->FillRectangle(&rect, brush);
|
|
break;
|
|
}
|
|
case CursorType::VerticalBar:
|
|
rect.right = rect.left + p.s->font->thinLineWidth;
|
|
renderTarget->FillRectangle(&rect, brush);
|
|
break;
|
|
case CursorType::Underscore:
|
|
rect.top += p.s->font->underline.position;
|
|
rect.bottom = rect.top + p.s->font->underline.height;
|
|
renderTarget->FillRectangle(&rect, brush);
|
|
break;
|
|
case CursorType::EmptyBox:
|
|
{
|
|
const auto w = static_cast<f32>(p.s->font->thinLineWidth);
|
|
const auto wh = w / 2.0f;
|
|
rect.left += wh;
|
|
rect.top += wh;
|
|
rect.right -= wh;
|
|
rect.bottom -= wh;
|
|
renderTarget->DrawRectangle(&rect, brush, w, nullptr);
|
|
break;
|
|
}
|
|
case CursorType::FullBox:
|
|
renderTarget->FillRectangle(&rect, brush);
|
|
break;
|
|
case CursorType::DoubleUnderscore:
|
|
{
|
|
auto rect2 = rect;
|
|
rect2.top = rect.top + p.s->font->doubleUnderline[0].position;
|
|
rect2.bottom = rect2.top + p.s->font->thinLineWidth;
|
|
renderTarget->FillRectangle(&rect2, brush);
|
|
rect.top = rect.top + p.s->font->doubleUnderline[1].position;
|
|
rect.bottom = rect.top + p.s->font->thinLineWidth;
|
|
renderTarget->FillRectangle(&rect, brush);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void BackendD2D::_drawSelection(const RenderingPayload& p)
|
|
{
|
|
u16 y = 0;
|
|
for (const auto& row : p.rows)
|
|
{
|
|
if (row->selectionTo > row->selectionFrom)
|
|
{
|
|
const D2D1_RECT_F rect{
|
|
static_cast<f32>(p.s->font->cellSize.x * row->selectionFrom),
|
|
static_cast<f32>(p.s->font->cellSize.y * y),
|
|
static_cast<f32>(p.s->font->cellSize.x * row->selectionTo),
|
|
static_cast<f32>(p.s->font->cellSize.y * (y + 1)),
|
|
};
|
|
_fillRectangle(rect, p.s->misc->selectionColor);
|
|
}
|
|
|
|
y++;
|
|
}
|
|
}
|
|
|
|
#if ATLAS_DEBUG_SHOW_DIRTY
|
|
void BackendD2D::_debugShowDirty(const RenderingPayload& p)
|
|
{
|
|
_presentRects[_presentRectsPos] = p.dirtyRectInPx;
|
|
_presentRectsPos = (_presentRectsPos + 1) % std::size(_presentRects);
|
|
|
|
for (size_t i = 0; i < std::size(_presentRects); ++i)
|
|
{
|
|
const auto& rect = _presentRects[(_presentRectsPos + i) % std::size(_presentRects)];
|
|
if (rect.non_empty())
|
|
{
|
|
const D2D1_RECT_F rectF{
|
|
static_cast<f32>(rect.left),
|
|
static_cast<f32>(rect.top),
|
|
static_cast<f32>(rect.right),
|
|
static_cast<f32>(rect.bottom),
|
|
};
|
|
const auto color = colorbrewer::pastel1[i] | 0x1f000000;
|
|
_fillRectangle(rectF, color);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if ATLAS_DEBUG_DUMP_RENDER_TARGET
|
|
void BackendD2D::_debugDumpRenderTarget(const RenderingPayload& p)
|
|
{
|
|
if (_dumpRenderTargetCounter == 0)
|
|
{
|
|
ExpandEnvironmentStringsW(ATLAS_DEBUG_DUMP_RENDER_TARGET_PATH, &_dumpRenderTargetBasePath[0], gsl::narrow_cast<DWORD>(std::size(_dumpRenderTargetBasePath)));
|
|
std::filesystem::create_directories(_dumpRenderTargetBasePath);
|
|
}
|
|
|
|
wchar_t path[MAX_PATH];
|
|
swprintf_s(path, L"%s\\%u_%08zu.png", &_dumpRenderTargetBasePath[0], GetCurrentProcessId(), _dumpRenderTargetCounter);
|
|
SaveTextureToPNG(_deviceContext.get(), _swapChainManager.GetBuffer().get(), p.s->font->dpi, &path[0]);
|
|
_dumpRenderTargetCounter++;
|
|
}
|
|
#endif
|
|
|
|
ID2D1SolidColorBrush* BackendD2D::_brushWithColor(u32 color)
|
|
{
|
|
if (_brushColor != color)
|
|
{
|
|
_brushWithColorUpdate(color);
|
|
}
|
|
return _brush.get();
|
|
}
|
|
|
|
ID2D1SolidColorBrush* BackendD2D::_brushWithColorUpdate(u32 color)
|
|
{
|
|
const auto d2dColor = colorFromU32(color);
|
|
_brush->SetColor(&d2dColor);
|
|
_brushColor = color;
|
|
return _brush.get();
|
|
}
|
|
|
|
void BackendD2D::_fillRectangle(const D2D1_RECT_F& rect, u32 color)
|
|
{
|
|
const auto brush = _brushWithColor(color);
|
|
_renderTarget->FillRectangle(&rect, brush);
|
|
}
|
|
|
|
TIL_FAST_MATH_END
|