mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-13 21:42:26 -06:00
- Various spelling fixes - Refresh metadata (including dictionaries) - Upgrade to v0.0.25 ## Validation Steps Performed - check-spelling has been automatically testing this repository for a while now on a daily basis to ensure that it works fairly reliably: https://github.com/check-spelling-sandbox/autotest-check-spelling/actions/workflows/microsoft-terminal-spelling2.yml Specific in-code fixes: - winget - whereas - tl;dr - set up - otherwise, - more, - macbook - its - invalid - in order to - if - if the - for this tab,... - fall back - course, - cch - aspect - archaeologists - an - all at once - a - `...` - ; otherwise, Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
1487 lines
56 KiB
C++
1487 lines
56 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
#include "renderer.hpp"
|
|
|
|
using namespace Microsoft::Console::Render;
|
|
using namespace Microsoft::Console::Types;
|
|
|
|
using PointTree = interval_tree::IntervalTree<til::point, size_t>;
|
|
|
|
static constexpr auto maxRetriesForRenderEngine = 3;
|
|
// The renderer will wait this number of milliseconds * how many tries have elapsed before trying again.
|
|
static constexpr auto renderBackoffBaseTimeMilliseconds{ 150 };
|
|
|
|
#define FOREACH_ENGINE(var) \
|
|
for (auto var : _engines) \
|
|
if (!var) \
|
|
break; \
|
|
else
|
|
|
|
// Routine Description:
|
|
// - Creates a new renderer controller for a console.
|
|
// Arguments:
|
|
// - pData - The interface to console data structures required for rendering
|
|
// Return Value:
|
|
// - An instance of a Renderer.
|
|
Renderer::Renderer(RenderSettings& renderSettings, IRenderData* pData) :
|
|
_renderSettings(renderSettings),
|
|
_pData(pData)
|
|
{
|
|
}
|
|
|
|
IRenderData* Renderer::GetRenderData() const noexcept
|
|
{
|
|
return _pData;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Walks through the console data structures to compose a new frame based on the data that has changed since last call and outputs it to the connected rendering engine.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - HRESULT S_OK, GDI error, Safe Math error, or state/argument errors.
|
|
[[nodiscard]] HRESULT Renderer::PaintFrame()
|
|
{
|
|
auto tries = maxRetriesForRenderEngine;
|
|
while (tries > 0)
|
|
{
|
|
// BODGY: Optimally we would want to retry per engine, but that causes different
|
|
// problems (intermittent inconsistent states between text renderer and UIA output,
|
|
// not being able to lock the cursor location, etc.).
|
|
const auto hr = _PaintFrame();
|
|
if (SUCCEEDED(hr))
|
|
{
|
|
break;
|
|
}
|
|
|
|
LOG_HR_IF(hr, hr != E_PENDING);
|
|
|
|
if (--tries == 0)
|
|
{
|
|
// Stop trying.
|
|
_thread.DisablePainting();
|
|
if (_pfnRendererEnteredErrorState)
|
|
{
|
|
_pfnRendererEnteredErrorState();
|
|
}
|
|
// If there's no callback, we still don't want to FAIL_FAST: the renderer going black
|
|
// isn't near as bad as the entire application aborting. We're a component. We shouldn't
|
|
// abort applications that host us.
|
|
return S_FALSE;
|
|
}
|
|
|
|
// Add a bit of backoff.
|
|
// Sleep 150ms, 300ms, 450ms before failing out and disabling the renderer.
|
|
Sleep(renderBackoffBaseTimeMilliseconds * (maxRetriesForRenderEngine - tries));
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT Renderer::_PaintFrame() noexcept
|
|
{
|
|
{
|
|
_pData->LockConsole();
|
|
auto unlock = wil::scope_exit([&]() {
|
|
_pData->UnlockConsole();
|
|
});
|
|
|
|
if (_isSynchronizingOutput)
|
|
{
|
|
_synchronizeWithOutput();
|
|
}
|
|
|
|
// Last chance check if anything scrolled without an explicit invalidate notification since the last frame.
|
|
_CheckViewportAndScroll();
|
|
|
|
_invalidateCurrentCursor(); // Invalidate the previous cursor position.
|
|
_invalidateOldComposition();
|
|
|
|
_updateCursorInfo();
|
|
_compositionCache.reset();
|
|
|
|
_invalidateCurrentCursor(); // Invalidate the new cursor position.
|
|
_prepareNewComposition();
|
|
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
RETURN_IF_FAILED(_PaintFrameForEngine(pEngine));
|
|
}
|
|
}
|
|
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
RETURN_IF_FAILED(pEngine->Present());
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT Renderer::_PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept
|
|
try
|
|
{
|
|
FAIL_FAST_IF_NULL(pEngine); // This is a programming error. Fail fast.
|
|
|
|
// Try to start painting a frame
|
|
const auto hr = pEngine->StartPaint();
|
|
RETURN_IF_FAILED(hr);
|
|
|
|
// Return early if there's nothing to paint.
|
|
// The renderer itself tracks if there's something to do with the title, the
|
|
// engine won't know that.
|
|
if (S_FALSE == hr)
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
auto endPaint = wil::scope_exit([&]() {
|
|
LOG_IF_FAILED(pEngine->EndPaint());
|
|
|
|
// If the engine tells us it really wants to redraw immediately,
|
|
// tell the thread so it doesn't go to sleep and ticks again
|
|
// at the next opportunity.
|
|
if (pEngine->RequiresContinuousRedraw())
|
|
{
|
|
NotifyPaintFrame();
|
|
}
|
|
});
|
|
|
|
// A. Prep Colors
|
|
RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, {}, false, true));
|
|
|
|
// B. Perform Scroll Operations
|
|
RETURN_IF_FAILED(_PerformScrolling(pEngine));
|
|
|
|
// C. Prepare the engine with additional information before we start drawing.
|
|
RETURN_IF_FAILED(_PrepareRenderInfo(pEngine));
|
|
|
|
// 1. Paint Background
|
|
RETURN_IF_FAILED(_PaintBackground(pEngine));
|
|
|
|
// 2. Paint Rows of Text
|
|
_PaintBufferOutput(pEngine);
|
|
|
|
// 4. Paint Selection
|
|
_PaintSelection(pEngine);
|
|
|
|
// 5. Paint Cursor
|
|
_PaintCursor(pEngine);
|
|
|
|
// 6. Paint window title
|
|
RETURN_IF_FAILED(_PaintTitle(pEngine));
|
|
|
|
// Force scope exit end paint to finish up collecting information and possibly painting
|
|
endPaint.reset();
|
|
|
|
// As we leave the scope, EndPaint will be called (declared above)
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN()
|
|
|
|
void Renderer::NotifyPaintFrame() noexcept
|
|
{
|
|
// The thread will provide throttling for us.
|
|
_thread.NotifyPaint();
|
|
}
|
|
|
|
// NOTE: You must be holding the console lock when calling this function.
|
|
void Renderer::SynchronizedOutputChanged() noexcept
|
|
{
|
|
const auto so = _renderSettings.GetRenderMode(RenderSettings::Mode::SynchronizedOutput);
|
|
if (_isSynchronizingOutput == so)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If `_isSynchronizingOutput` is true, it'll kick the
|
|
// render thread into calling `_synchronizeWithOutput()`...
|
|
_isSynchronizingOutput = so;
|
|
|
|
if (!_isSynchronizingOutput)
|
|
{
|
|
// ...otherwise, unblock `_synchronizeWithOutput()` from the `WaitOnAddress` call.
|
|
WakeByAddressSingle(&_isSynchronizingOutput);
|
|
|
|
// It's crucial to give the render thread at least a chance to gain the lock.
|
|
// Otherwise, a VT application could continuously spam DECSET 2026 (Synchronized Output) and
|
|
// essentially drop our renderer to 10 FPS, because `_isSynchronizingOutput` is always true.
|
|
//
|
|
// Obviously calling LockConsole/UnlockConsole here is an awful, ugly hack,
|
|
// since there's no guarantee that this is the same lock as the one the VT parser uses.
|
|
// But the alternative is Denial-Of-Service of the render thread.
|
|
//
|
|
// Note that this causes raw throughput of DECSET 2026 to be comparatively low, but that's fine.
|
|
// Apps that use DECSET 2026 don't produce that sequence continuously, but rather at a fixed rate.
|
|
_pData->UnlockConsole();
|
|
_pData->LockConsole();
|
|
}
|
|
}
|
|
|
|
void Renderer::_synchronizeWithOutput() noexcept
|
|
{
|
|
constexpr DWORD timeout = 100;
|
|
|
|
UINT64 start = 0;
|
|
DWORD elapsed = 0;
|
|
bool wrong = false;
|
|
|
|
QueryUnbiasedInterruptTime(&start);
|
|
|
|
// Wait for `_isSynchronizingOutput` to be set to false or for a timeout to occur.
|
|
while (true)
|
|
{
|
|
// We can't call a blocking function while holding the console lock, so release it temporarily.
|
|
_pData->UnlockConsole();
|
|
const auto ok = WaitOnAddress(&_isSynchronizingOutput, &wrong, sizeof(_isSynchronizingOutput), timeout - elapsed);
|
|
_pData->LockConsole();
|
|
|
|
if (!ok || !_isSynchronizingOutput)
|
|
{
|
|
break;
|
|
}
|
|
|
|
UINT64 now;
|
|
QueryUnbiasedInterruptTime(&now);
|
|
elapsed = static_cast<DWORD>((now - start) / 10000);
|
|
if (elapsed >= timeout)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If a timeout occurred, `_isSynchronizingOutput` may still be true.
|
|
// Set it to false now to skip calling `_synchronizeWithOutput()` on the next frame.
|
|
_isSynchronizingOutput = false;
|
|
_renderSettings.SetRenderMode(RenderSettings::Mode::SynchronizedOutput, false);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when the system has requested we redraw a portion of the console.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerSystemRedraw(const til::rect* const prcDirtyClient)
|
|
{
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateSystem(prcDirtyClient));
|
|
}
|
|
|
|
NotifyPaintFrame();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when a particular region within the console buffer has changed.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerRedraw(const Viewport& region)
|
|
{
|
|
auto view = _pData->GetViewport();
|
|
auto srUpdateRegion = region.ToExclusive();
|
|
|
|
// If the dirty region has double width lines, we need to double the size of
|
|
// the right margin to make sure all the affected cells are invalidated.
|
|
const auto& buffer = _pData->GetTextBuffer();
|
|
for (auto row = srUpdateRegion.top; row < srUpdateRegion.bottom; row++)
|
|
{
|
|
if (buffer.IsDoubleWidthLine(row))
|
|
{
|
|
srUpdateRegion.right *= 2;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (view.TrimToViewport(&srUpdateRegion))
|
|
{
|
|
view.ConvertToOrigin(&srUpdateRegion);
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->Invalidate(&srUpdateRegion));
|
|
}
|
|
|
|
NotifyPaintFrame();
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when a particular coordinate within the console buffer has changed.
|
|
// Arguments:
|
|
// - pcoord: The buffer-space coordinate that has changed.
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerRedraw(const til::point* const pcoord)
|
|
{
|
|
TriggerRedraw(Viewport::FromDimensions(*pcoord, { 1, 1 })); // this will notify to paint if we need it.
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when something that changes the output state has occurred and the entire frame is now potentially invalid.
|
|
// - NOTE: Use sparingly. Try to reduce the refresh region where possible. Only use when a global state change has occurred.
|
|
// Arguments:
|
|
// - backgroundChanged - Set to true if the background color has changed.
|
|
// - frameChanged - Set to true if the frame colors have changed.
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerRedrawAll(const bool backgroundChanged, const bool frameChanged)
|
|
{
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateAll());
|
|
}
|
|
|
|
NotifyPaintFrame();
|
|
|
|
if (backgroundChanged && _pfnBackgroundColorChanged)
|
|
{
|
|
_pfnBackgroundColorChanged();
|
|
}
|
|
|
|
if (frameChanged && _pfnFrameColorChanged)
|
|
{
|
|
_pfnFrameColorChanged();
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Called when the host is about to die, to give the renderer one last chance
|
|
// to paint before the host exits.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerTeardown() noexcept
|
|
{
|
|
// We need to shut down the paint thread on teardown.
|
|
_thread.TriggerTeardown();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when the selected area in the console has changed.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerSelection()
|
|
try
|
|
{
|
|
const auto spans = _pData->GetSelectionSpans();
|
|
if (spans.size() != _lastSelectionPaintSize || (!spans.empty() && _lastSelectionPaintSpan != til::point_span{ spans.front().start, spans.back().end }))
|
|
{
|
|
std::vector<til::rect> newSelectionViewportRects;
|
|
|
|
_lastSelectionPaintSize = spans.size();
|
|
if (_lastSelectionPaintSize)
|
|
{
|
|
_lastSelectionPaintSpan = til::point_span{ spans.front().start, spans.back().end };
|
|
|
|
const auto& buffer = _pData->GetTextBuffer();
|
|
const auto bufferWidth = buffer.GetSize().Width();
|
|
const til::rect vp{ _viewport.ToExclusive() };
|
|
for (auto&& sp : spans)
|
|
{
|
|
sp.iterate_rows_exclusive(bufferWidth, [&](til::CoordType row, til::CoordType min, til::CoordType max) {
|
|
const auto shift = buffer.GetLineRendition(row) != LineRendition::SingleWidth ? 1 : 0;
|
|
min <<= shift;
|
|
max <<= shift;
|
|
til::rect r{ min, row, max, row + 1 };
|
|
newSelectionViewportRects.emplace_back(r.to_origin(vp));
|
|
});
|
|
}
|
|
}
|
|
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateSelection(_lastSelectionRectsByViewport));
|
|
LOG_IF_FAILED(pEngine->InvalidateSelection(newSelectionViewportRects));
|
|
}
|
|
|
|
std::exchange(_lastSelectionRectsByViewport, newSelectionViewportRects);
|
|
|
|
NotifyPaintFrame();
|
|
}
|
|
}
|
|
CATCH_LOG()
|
|
|
|
// Routine Description:
|
|
// - Called when the search highlight areas in the console have changed.
|
|
void Renderer::TriggerSearchHighlight(const std::vector<til::point_span>& oldHighlights)
|
|
try
|
|
{
|
|
// no need to invalidate focused search highlight separately as they are
|
|
// included in (all) search highlights.
|
|
const auto newHighlights = _pData->GetSearchHighlights();
|
|
|
|
if (oldHighlights.empty() && newHighlights.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto& buffer = _pData->GetTextBuffer();
|
|
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateHighlight(oldHighlights, buffer));
|
|
LOG_IF_FAILED(pEngine->InvalidateHighlight(newHighlights, buffer));
|
|
}
|
|
|
|
NotifyPaintFrame();
|
|
}
|
|
CATCH_LOG()
|
|
|
|
// Routine Description:
|
|
// - Called when we want to check if the viewport has moved and scroll accordingly if so.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - True if something changed and we scrolled. False otherwise.
|
|
bool Renderer::_CheckViewportAndScroll()
|
|
{
|
|
const auto srOldViewport = _viewport.ToInclusive();
|
|
const auto srNewViewport = _pData->GetViewport().ToInclusive();
|
|
|
|
if (!_forceUpdateViewport && srOldViewport == srNewViewport)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_viewport = Viewport::FromInclusive(srNewViewport);
|
|
_forceUpdateViewport = false;
|
|
|
|
til::point coordDelta;
|
|
coordDelta.x = srOldViewport.left - srNewViewport.left;
|
|
coordDelta.y = srOldViewport.top - srNewViewport.top;
|
|
|
|
FOREACH_ENGINE(engine)
|
|
{
|
|
LOG_IF_FAILED(engine->UpdateViewport(srNewViewport));
|
|
LOG_IF_FAILED(engine->InvalidateScroll(&coordDelta));
|
|
}
|
|
|
|
_ScrollPreviousSelection(coordDelta);
|
|
|
|
// The cursor may have moved out of or into the viewport. Update the .inViewport property.
|
|
{
|
|
const auto view = ScreenToBufferLine(srNewViewport, _currentCursorOptions.lineRendition);
|
|
auto coordCursor = _currentCursorOptions.coordCursor;
|
|
|
|
// `coordCursor` was stored in viewport-relative while `view` is in absolute coordinates.
|
|
// --> Turn it back into the absolute coordinates with the help of the viewport.
|
|
// We have to use the new viewport, because _ScrollPreviousSelection adjusts the cursor position to match the new one.
|
|
coordCursor.y += srNewViewport.top;
|
|
|
|
// Note that we allow the X coordinate to be outside the left border by 1 position,
|
|
// because the cursor could still be visible if the focused character is double width.
|
|
const auto xInRange = coordCursor.x >= view.left - 1 && coordCursor.x <= view.right;
|
|
const auto yInRange = coordCursor.y >= view.top && coordCursor.y <= view.bottom;
|
|
|
|
_currentCursorOptions.inViewport = xInRange && yInRange;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when a scroll operation has occurred by manipulating the viewport.
|
|
// - This is a special case as calling out scrolls explicitly drastically improves performance.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerScroll()
|
|
{
|
|
if (_CheckViewportAndScroll())
|
|
{
|
|
NotifyPaintFrame();
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when a scroll operation wishes to explicitly adjust the frame by the given coordinate distance.
|
|
// - This is a special case as calling out scrolls explicitly drastically improves performance.
|
|
// - This should only be used when the viewport is not modified. It lets us know we can "scroll anyway" to save perf,
|
|
// because the backing circular buffer rotated out from behind the viewport.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerScroll(const til::point* const pcoordDelta)
|
|
{
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateScroll(pcoordDelta));
|
|
}
|
|
|
|
_ScrollPreviousSelection(*pcoordDelta);
|
|
|
|
NotifyPaintFrame();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when the title of the console window has changed. Indicates that we
|
|
// should update the title on the next frame.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerTitleChange()
|
|
{
|
|
const auto newTitle = _pData->GetConsoleTitle();
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateTitle(newTitle));
|
|
}
|
|
NotifyPaintFrame();
|
|
}
|
|
|
|
void Renderer::TriggerNewTextNotification(const std::wstring_view newText)
|
|
{
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->NotifyNewText(newText));
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Update the title for a particular engine.
|
|
// Arguments:
|
|
// - pEngine: the engine to update the title for.
|
|
// Return Value:
|
|
// - the HRESULT of the underlying engine's UpdateTitle call.
|
|
HRESULT Renderer::_PaintTitle(IRenderEngine* const pEngine)
|
|
{
|
|
const auto newTitle = _pData->GetConsoleTitle();
|
|
return pEngine->UpdateTitle(newTitle);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when a change in font or DPI has been detected.
|
|
// Arguments:
|
|
// - iDpi - New DPI value
|
|
// - FontInfoDesired - A description of the font we would like to have.
|
|
// - FontInfo - Data that will be fixed up/filled on return with the chosen font data.
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerFontChange(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo)
|
|
{
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->UpdateDpi(iDpi));
|
|
LOG_IF_FAILED(pEngine->UpdateFont(FontInfoDesired, FontInfo));
|
|
}
|
|
|
|
NotifyPaintFrame();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when the active soft font has been updated.
|
|
// Arguments:
|
|
// - bitPattern - An array of scanlines representing all the glyphs in the font.
|
|
// - cellSize - The cell size for an individual glyph.
|
|
// - centeringHint - The horizontal extent that glyphs are offset from center.
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::UpdateSoftFont(const std::span<const uint16_t> bitPattern, const til::size cellSize, const size_t centeringHint)
|
|
{
|
|
// We reserve PUA code points U+EF20 to U+EF7F for soft fonts, but the range
|
|
// that we test for in _IsSoftFontChar will depend on the size of the active
|
|
// bitPattern. If it's empty (i.e. no soft font is set), then nothing will
|
|
// match, and those code points will be treated the same as everything else.
|
|
const auto softFontCharCount = cellSize.height ? bitPattern.size() / cellSize.height : 0;
|
|
_lastSoftFontChar = _firstSoftFontChar + softFontCharCount - 1;
|
|
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->UpdateSoftFont(bitPattern, cellSize, centeringHint));
|
|
}
|
|
TriggerRedrawAll();
|
|
}
|
|
|
|
// We initially tried to have a "_isSoftFontChar" member function, but MSVC
|
|
// failed to inline it at _all_ call sites (check invocations inside loops).
|
|
// This issue strangely doesn't occur with static functions.
|
|
bool Renderer::s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar)
|
|
{
|
|
return v.size() == 1 && v[0] >= firstSoftFontChar && v[0] <= lastSoftFontChar;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Get the information on what font we would be using if we decided to create a font with the given parameters
|
|
// - This is for use with speculative calculations.
|
|
// Arguments:
|
|
// - iDpi - The DPI of the target display
|
|
// - pFontInfoDesired - A description of the font we would like to have.
|
|
// - pFontInfo - Data that will be fixed up/filled on return with the chosen font data.
|
|
// Return Value:
|
|
// - S_OK if set successfully or relevant GDI error via HRESULT.
|
|
[[nodiscard]] HRESULT Renderer::GetProposedFont(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo)
|
|
{
|
|
// There will only every really be two engines - the real head and the VT
|
|
// renderer. We won't know which is which, so iterate over them.
|
|
// Only return the result of the successful one if it's not S_FALSE (which is the VT renderer)
|
|
// TODO: 14560740 - The Window might be able to get at this info in a more sane manner
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
const auto hr = LOG_IF_FAILED(pEngine->GetProposedFont(FontInfoDesired, FontInfo, iDpi));
|
|
// We're looking for specifically S_OK, S_FALSE is not good enough.
|
|
if (hr == S_OK)
|
|
{
|
|
return hr;
|
|
}
|
|
}
|
|
|
|
return E_FAIL;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Tests against the current rendering engine to see if this particular character would be considered
|
|
// full-width (inscribed in a square, twice as wide as a standard Western character, typically used for CJK
|
|
// languages) or half-width.
|
|
// - Typically used to determine how many positions in the backing buffer a particular character should fill.
|
|
// NOTE: This only handles 1 or 2 wide (in monospace terms) characters.
|
|
// Arguments:
|
|
// - glyph - the utf16 encoded codepoint to test
|
|
// Return Value:
|
|
// - True if the codepoint is full-width (two wide), false if it is half-width (one wide).
|
|
bool Renderer::IsGlyphWideByFont(const std::wstring_view glyph)
|
|
{
|
|
auto fIsFullWidth = false;
|
|
|
|
// There will only every really be two engines - the real head and the VT
|
|
// renderer. We won't know which is which, so iterate over them.
|
|
// Only return the result of the successful one if it's not S_FALSE (which is the VT renderer)
|
|
// TODO: 14560740 - The Window might be able to get at this info in a more sane manner
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
const auto hr = LOG_IF_FAILED(pEngine->IsGlyphWideByFont(glyph, &fIsFullWidth));
|
|
// We're looking for specifically S_OK, S_FALSE is not good enough.
|
|
if (hr == S_OK)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return fIsFullWidth;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Sets an event in the render thread that allows it to proceed, thus enabling painting.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::EnablePainting()
|
|
{
|
|
// When the renderer is constructed, the initial viewport won't be available yet,
|
|
// but once EnablePainting is called it should be safe to retrieve.
|
|
_viewport = _pData->GetViewport();
|
|
_thread.EnablePainting();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paint helper to fill in the background color of the invalid area within the frame.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
[[nodiscard]] HRESULT Renderer::_PaintBackground(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
return pEngine->PaintBackground();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paint helper to copy the primary console buffer text onto the screen.
|
|
// - This portion primarily handles figuring the current viewport, comparing it/trimming it versus the invalid portion of the frame, and queuing up, row by row, which pieces of text need to be further processed.
|
|
// - See also: Helper functions that separate out each complexity of text rendering.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
// This is the subsection of the entire screen buffer that is currently being presented.
|
|
// It can move left/right or top/bottom depending on how the viewport is scrolled
|
|
// relative to the entire buffer.
|
|
const auto view = _pData->GetViewport();
|
|
const auto compositionRow = _compositionCache ? _compositionCache->absoluteOrigin.y : -1;
|
|
const auto& activeComposition = _pData->GetActiveComposition();
|
|
|
|
// This is effectively the number of cells on the visible screen that need to be redrawn.
|
|
// The origin is always 0, 0 because it represents the screen itself, not the underlying buffer.
|
|
std::span<const til::rect> dirtyAreas;
|
|
LOG_IF_FAILED(pEngine->GetDirtyArea(dirtyAreas));
|
|
|
|
// This is to make sure any transforms are reset when this paint is finished.
|
|
auto resetLineTransform = wil::scope_exit([&]() {
|
|
LOG_IF_FAILED(pEngine->ResetLineTransform());
|
|
});
|
|
|
|
for (const auto& dirtyRect : dirtyAreas)
|
|
{
|
|
if (!dirtyRect)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto dirty = Viewport::FromExclusive(dirtyRect);
|
|
|
|
// Shift the origin of the dirty region to match the underlying buffer so we can
|
|
// compare the two regions directly for intersection.
|
|
dirty = Viewport::Offset(dirty, view.Origin());
|
|
|
|
// The intersection between what is dirty on the screen (in need of repaint)
|
|
// and what is supposed to be visible on the screen (the viewport) is what
|
|
// we need to walk through line-by-line and repaint onto the screen.
|
|
const auto redraw = Viewport::Intersect(dirty, view);
|
|
|
|
// Retrieve the text buffer so we can read information out of it.
|
|
auto& buffer = _pData->GetTextBuffer();
|
|
// Now walk through each row of text that we need to redraw.
|
|
for (auto row = redraw.Top(); row < redraw.BottomExclusive(); row++)
|
|
{
|
|
// Calculate the boundaries of a single line. This is from the left to right edge of the dirty
|
|
// area in width and exactly 1 tall.
|
|
const auto screenLine = til::inclusive_rect{ redraw.Left(), row, redraw.RightInclusive(), row };
|
|
const auto& r = buffer.GetRowByOffset(row);
|
|
|
|
// Draw the active composition.
|
|
// We have to use some tricks here with const_cast, because the code after it relies on TextBufferCellIterator,
|
|
// which isn't compatible with the scratchpad row. This forces us to back up and modify the actual row `r`.
|
|
ROW* rowBackup = nullptr;
|
|
if (row == compositionRow)
|
|
{
|
|
auto& scratch = buffer.GetScratchpadRow();
|
|
scratch.CopyFrom(r);
|
|
rowBackup = &scratch;
|
|
|
|
std::wstring_view text{ activeComposition.text };
|
|
RowWriteState state{
|
|
.columnLimit = r.GetReadableColumnCount(),
|
|
.columnEnd = _compositionCache->absoluteOrigin.x,
|
|
};
|
|
|
|
size_t off = 0;
|
|
for (const auto& range : activeComposition.attributes)
|
|
{
|
|
const auto len = range.len;
|
|
auto attr = range.attr;
|
|
|
|
// Use the color at the cursor if TSF didn't specify any explicit color.
|
|
if (attr.GetBackground().IsDefault())
|
|
{
|
|
attr.SetBackground(_compositionCache->baseAttribute.GetBackground());
|
|
}
|
|
if (attr.GetForeground().IsDefault())
|
|
{
|
|
attr.SetForeground(_compositionCache->baseAttribute.GetForeground());
|
|
}
|
|
|
|
state.text = text.substr(off, len);
|
|
state.columnBegin = state.columnEnd;
|
|
const_cast<ROW&>(r).ReplaceText(state);
|
|
const_cast<ROW&>(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr);
|
|
off += len;
|
|
}
|
|
}
|
|
const auto restore = wil::scope_exit([&] {
|
|
if (rowBackup)
|
|
{
|
|
const_cast<ROW&>(r).CopyFrom(*rowBackup);
|
|
}
|
|
});
|
|
|
|
// Convert the screen coordinates of the line to an equivalent
|
|
// range of buffer cells, taking line rendition into account.
|
|
const auto lineRendition = buffer.GetLineRendition(row);
|
|
const auto bufferLine = Viewport::FromInclusive(ScreenToBufferLine(screenLine, lineRendition));
|
|
|
|
// Find where on the screen we should place this line information. This requires us to re-map
|
|
// the buffer-based origin of the line back onto the screen-based origin of the line.
|
|
// For example, the screen might say we need to paint line 1 because it is dirty but the viewport
|
|
// is actually looking at line 26 relative to the buffer. This means that we need line 27 out
|
|
// of the backing buffer to fill in line 1 of the screen.
|
|
const auto screenPosition = bufferLine.Origin() - til::point{ 0, view.Top() };
|
|
|
|
// Retrieve the cell information iterator limited to just this line we want to redraw.
|
|
auto it = buffer.GetCellDataAt(bufferLine.Origin(), bufferLine);
|
|
|
|
// Calculate if two things are true:
|
|
// 1. this row wrapped
|
|
// 2. We're painting the last col of the row.
|
|
// In that case, set lineWrapped=true for the _PaintBufferOutputHelper call.
|
|
const auto lineWrapped = (buffer.GetRowByOffset(bufferLine.Origin().y).WasWrapForced()) &&
|
|
(bufferLine.RightExclusive() == buffer.GetSize().Width());
|
|
|
|
// Prepare the appropriate line transform for the current row and viewport offset.
|
|
LOG_IF_FAILED(pEngine->PrepareLineTransform(lineRendition, screenPosition.y, view.Left()));
|
|
|
|
// Ask the helper to paint through this specific line.
|
|
_PaintBufferOutputHelper(pEngine, it, screenPosition, lineWrapped);
|
|
|
|
// Paint any image content on top of the text.
|
|
const auto imageSlice = buffer.GetRowByOffset(row).GetImageSlice();
|
|
if (imageSlice) [[unlikely]]
|
|
{
|
|
LOG_IF_FAILED(pEngine->PaintImageSlice(*imageSlice, screenPosition.y, view.Left()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool _IsAllSpaces(const std::wstring_view v)
|
|
{
|
|
// first non-space char is not found (is npos)
|
|
return v.find_first_not_of(L' ') == decltype(v)::npos;
|
|
}
|
|
|
|
void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine,
|
|
TextBufferCellIterator it,
|
|
const til::point target,
|
|
const bool lineWrapped)
|
|
{
|
|
auto globalInvert{ _renderSettings.GetRenderMode(RenderSettings::Mode::ScreenReversed) };
|
|
|
|
// If we have valid data, let's figure out how to draw it.
|
|
if (it)
|
|
{
|
|
// TODO: MSFT: 20961091 - This is a perf issue. Instead of rebuilding this and allocing memory to hold the reinterpretation,
|
|
// we should have an iterator/view adapter for the rendering.
|
|
// That would probably also eliminate the RenderData needing to give us the entire TextBuffer as well...
|
|
// Retrieve the iterator for one line of information.
|
|
til::CoordType cols = 0;
|
|
|
|
// Retrieve the first color.
|
|
auto color = it->TextAttr();
|
|
// Retrieve the first pattern id
|
|
auto patternIds = _pData->GetPatternId(target);
|
|
// Determine whether we're using a soft font.
|
|
auto usingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar);
|
|
|
|
// And hold the point where we should start drawing.
|
|
auto screenPoint = target;
|
|
|
|
// This outer loop will continue until we reach the end of the text we are trying to draw.
|
|
while (it)
|
|
{
|
|
// Hold onto the current run color right here for the length of the outer loop.
|
|
// We'll be changing the persistent one as we run through the inner loops to detect
|
|
// when a run changes, but we will still need to know this color at the bottom
|
|
// when we go to draw gridlines for the length of the run.
|
|
const auto currentRunColor = color;
|
|
|
|
// Hold onto the current pattern id as well
|
|
const auto currentPatternId = patternIds;
|
|
|
|
// Update the drawing brushes with our color and font usage.
|
|
THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, usingSoftFont, false));
|
|
|
|
// Advance the point by however many columns we've just outputted and reset the accumulator.
|
|
screenPoint.x += cols;
|
|
cols = 0;
|
|
|
|
// Hold onto the start of this run iterator and the target location where we started
|
|
// in case we need to do some special work to paint the line drawing characters.
|
|
const auto currentRunItStart = it;
|
|
const auto currentRunTargetStart = screenPoint;
|
|
|
|
// Ensure that our cluster vector is clear.
|
|
_clusterBuffer.clear();
|
|
|
|
// Reset our flag to know when we're in the special circumstance
|
|
// of attempting to draw only the right-half of a two-column character
|
|
// as the first item in our run.
|
|
auto trimLeft = false;
|
|
|
|
// Run contains wide character (>1 columns)
|
|
auto containsWideCharacter = false;
|
|
|
|
// This inner loop will accumulate clusters until the color changes.
|
|
// When the color changes, it will save the new color off and break.
|
|
// We also accumulate clusters according to regex patterns
|
|
do
|
|
{
|
|
til::point thisPoint{ screenPoint.x + cols, screenPoint.y };
|
|
const auto thisPointPatterns = _pData->GetPatternId(thisPoint);
|
|
const auto thisUsingSoftFont = s_IsSoftFontChar(it->Chars(), _firstSoftFontChar, _lastSoftFontChar);
|
|
const auto changedPatternOrFont = patternIds != thisPointPatterns || usingSoftFont != thisUsingSoftFont;
|
|
if (color != it->TextAttr() || changedPatternOrFont)
|
|
{
|
|
auto newAttr{ it->TextAttr() };
|
|
// foreground doesn't matter for runs of spaces (!)
|
|
// if we trick it . . . we call Paint far fewer times for cmatrix
|
|
if (!_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, globalInvert) || changedPatternOrFont)
|
|
{
|
|
color = newAttr;
|
|
patternIds = thisPointPatterns;
|
|
usingSoftFont = thisUsingSoftFont;
|
|
break; // vend this run
|
|
}
|
|
}
|
|
|
|
// Walk through the text data and turn it into rendering clusters.
|
|
// Keep the columnCount as we go to improve performance over digging it out of the vector at the end.
|
|
auto columnCount = it->Columns();
|
|
|
|
// If we're on the first cluster to be added and it's marked as "trailing"
|
|
// (a.k.a. the right half of a two column character), then we need some special handling.
|
|
if (_clusterBuffer.empty() && it->DbcsAttr() == DbcsAttribute::Trailing)
|
|
{
|
|
// Move left to the one so the whole character can be struck correctly.
|
|
--screenPoint.x;
|
|
// And tell the next function to trim off the left half of it.
|
|
trimLeft = true;
|
|
// And add one to the number of columns we expect it to take as we insert it.
|
|
++columnCount;
|
|
}
|
|
|
|
if (columnCount > 1)
|
|
{
|
|
containsWideCharacter = true;
|
|
}
|
|
|
|
// Advance the cluster and column counts.
|
|
_clusterBuffer.emplace_back(it->Chars(), columnCount);
|
|
it += std::max(it->Columns(), 1); // prevent infinite loop for no visible columns
|
|
cols += columnCount;
|
|
|
|
} while (it);
|
|
|
|
// Do the painting.
|
|
THROW_IF_FAILED(pEngine->PaintBufferLine({ _clusterBuffer.data(), _clusterBuffer.size() }, screenPoint, trimLeft, lineWrapped));
|
|
|
|
// If we're allowed to do grid drawing, draw that now too (since it will be coupled with the color data)
|
|
// We're only allowed to draw the grid lines under certain circumstances.
|
|
if (_pData->IsGridLineDrawingAllowed())
|
|
{
|
|
// See GH: 803
|
|
// If we found a wide character while we looped above, it's possible we skipped over the right half
|
|
// attribute that could have contained different line information than the left half.
|
|
if (containsWideCharacter)
|
|
{
|
|
// Start from the original position in this run.
|
|
auto lineIt = currentRunItStart;
|
|
// Start from the original target in this run.
|
|
auto lineTarget = currentRunTargetStart;
|
|
|
|
// We need to go through the iterators again to ensure we get the lines associated with each
|
|
// exact column. The code above will condense two-column characters into one, but it is possible
|
|
// (like with the IME) that the line drawing characters will vary from the left to right half
|
|
// of a wider character.
|
|
// We could theoretically pre-pass for this in the loop above to be more efficient about walking
|
|
// the iterator, but I fear it would make the code even more confusing than it already is.
|
|
// Do that in the future if some WPR trace points you to this spot as super bad.
|
|
for (til::CoordType colsPainted = 0; colsPainted < cols; ++colsPainted, ++lineIt, ++lineTarget.x)
|
|
{
|
|
auto lines = lineIt->TextAttr();
|
|
_PaintBufferOutputGridLineHelper(pEngine, lines, 1, lineTarget);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If nothing exciting is going on, draw the lines in bulk.
|
|
_PaintBufferOutputGridLineHelper(pEngine, currentRunColor, cols, screenPoint);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Generates a GridLines structure from the values in the
|
|
// provided textAttribute
|
|
// Arguments:
|
|
// - textAttribute: the TextAttribute to generate GridLines from.
|
|
// Return Value:
|
|
// - a GridLineSet containing all the gridline info from the TextAttribute
|
|
GridLineSet Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept
|
|
{
|
|
// Convert console grid line representations into rendering engine enum representations.
|
|
GridLineSet lines;
|
|
|
|
if (textAttribute.IsTopHorizontalDisplayed())
|
|
{
|
|
lines.set(GridLines::Top);
|
|
}
|
|
|
|
if (textAttribute.IsBottomHorizontalDisplayed())
|
|
{
|
|
lines.set(GridLines::Bottom);
|
|
}
|
|
|
|
if (textAttribute.IsLeftVerticalDisplayed())
|
|
{
|
|
lines.set(GridLines::Left);
|
|
}
|
|
|
|
if (textAttribute.IsRightVerticalDisplayed())
|
|
{
|
|
lines.set(GridLines::Right);
|
|
}
|
|
|
|
if (textAttribute.IsCrossedOut())
|
|
{
|
|
lines.set(GridLines::Strikethrough);
|
|
}
|
|
|
|
const auto underlineStyle = textAttribute.GetUnderlineStyle();
|
|
switch (underlineStyle)
|
|
{
|
|
case UnderlineStyle::NoUnderline:
|
|
break;
|
|
case UnderlineStyle::SinglyUnderlined:
|
|
lines.set(GridLines::Underline);
|
|
break;
|
|
case UnderlineStyle::DoublyUnderlined:
|
|
lines.set(GridLines::DoubleUnderline);
|
|
break;
|
|
case UnderlineStyle::CurlyUnderlined:
|
|
lines.set(GridLines::CurlyUnderline);
|
|
break;
|
|
case UnderlineStyle::DottedUnderlined:
|
|
lines.set(GridLines::DottedUnderline);
|
|
break;
|
|
case UnderlineStyle::DashedUnderlined:
|
|
lines.set(GridLines::DashedUnderline);
|
|
break;
|
|
default:
|
|
lines.set(GridLines::Underline);
|
|
break;
|
|
}
|
|
|
|
if (textAttribute.IsHyperlink())
|
|
{
|
|
lines.set(GridLines::HyperlinkUnderline);
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paint helper for primary buffer output function.
|
|
// - This particular helper sets up the various box drawing lines that can be inscribed around any character in the buffer (left, right, top, underline).
|
|
// - See also: All related helpers and buffer output functions.
|
|
// Arguments:
|
|
// - textAttribute - The line/box drawing attributes to use for this particular run.
|
|
// - cchLine - The length of both pwsLine and pbKAttrsLine.
|
|
// - coordTarget - The X/Y coordinate position in the buffer which we're attempting to start rendering from.
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine,
|
|
const TextAttribute textAttribute,
|
|
const size_t cchLine,
|
|
const til::point coordTarget)
|
|
{
|
|
// Convert console grid line representations into rendering engine enum representations.
|
|
auto lines = Renderer::s_GetGridlines(textAttribute);
|
|
|
|
// For now, we dash underline patterns and switch to regular underline on hover
|
|
if (_isHoveredHyperlink(textAttribute) || _isInHoveredInterval(coordTarget))
|
|
{
|
|
lines.reset(GridLines::HyperlinkUnderline);
|
|
lines.set(GridLines::Underline);
|
|
}
|
|
|
|
// Return early if there are no lines to paint.
|
|
if (lines.any())
|
|
{
|
|
// Get the current foreground and underline colors to render the lines.
|
|
const auto fg = _renderSettings.GetAttributeColors(textAttribute).first;
|
|
const auto underlineColor = _renderSettings.GetAttributeUnderlineColor(textAttribute);
|
|
// Draw the lines
|
|
LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, fg, underlineColor, cchLine, coordTarget));
|
|
}
|
|
}
|
|
|
|
bool Renderer::_isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept
|
|
{
|
|
return _hyperlinkHoveredId && _hyperlinkHoveredId == textAttribute.GetHyperlinkId();
|
|
}
|
|
|
|
bool Renderer::_isInHoveredInterval(const til::point coordTarget) const noexcept
|
|
{
|
|
return _hoveredInterval &&
|
|
_hoveredInterval->start <= coordTarget && coordTarget <= _hoveredInterval->stop &&
|
|
_pData->GetPatternId(coordTarget).size() > 0;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieve information about the cursor, and pack it into a CursorOptions
|
|
// which the render engine can use for painting the cursor.
|
|
// - If the cursor is "off", or the cursor is out of bounds of the viewport,
|
|
// this will return nullopt (indicating the cursor shouldn't be painted this
|
|
// frame)
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - nullopt if the cursor is off or out-of-frame; otherwise, a CursorOptions
|
|
void Renderer::_updateCursorInfo()
|
|
{
|
|
// Get cursor position in buffer
|
|
auto coordCursor = _pData->GetCursorPosition();
|
|
|
|
// GH#3166: Only draw the cursor if it's actually in the viewport. It
|
|
// might be on the line that's in that partially visible row at the
|
|
// bottom of the viewport, the space that's not quite a full line in
|
|
// height. Since we don't draw that text, we shouldn't draw the cursor
|
|
// there either.
|
|
|
|
// The cursor is never rendered as double height, so we don't care about
|
|
// the exact line rendition - only whether it's double width or not.
|
|
const auto doubleWidth = _pData->GetTextBuffer().IsDoubleWidthLine(coordCursor.y);
|
|
const auto lineRendition = doubleWidth ? LineRendition::DoubleWidth : LineRendition::SingleWidth;
|
|
|
|
// We need to convert the screen coordinates of the viewport to an
|
|
// equivalent range of buffer cells, taking line rendition into account.
|
|
const auto viewport = _pData->GetViewport().ToInclusive();
|
|
const auto view = ScreenToBufferLine(viewport, lineRendition);
|
|
|
|
// Note that we allow the X coordinate to be outside the left border by 1 position,
|
|
// because the cursor could still be visible if the focused character is double width.
|
|
const auto xInRange = coordCursor.x >= view.left - 1 && coordCursor.x <= view.right;
|
|
const auto yInRange = coordCursor.y >= view.top && coordCursor.y <= view.bottom;
|
|
|
|
// Adjust cursor Y offset to viewport.
|
|
// The viewport X offset is saved in the options and handled with a transform.
|
|
coordCursor.y -= view.top;
|
|
|
|
const auto cursorColor = _renderSettings.GetColorTableEntry(TextColor::CURSOR_COLOR);
|
|
const auto useColor = cursorColor != INVALID_COLOR;
|
|
|
|
_currentCursorOptions.coordCursor = coordCursor;
|
|
_currentCursorOptions.viewportLeft = viewport.left;
|
|
_currentCursorOptions.lineRendition = lineRendition;
|
|
_currentCursorOptions.ulCursorHeightPercent = _pData->GetCursorHeight();
|
|
_currentCursorOptions.cursorPixelWidth = _pData->GetCursorPixelWidth();
|
|
_currentCursorOptions.fIsDoubleWidth = _pData->IsCursorDoubleWidth();
|
|
_currentCursorOptions.cursorType = _pData->GetCursorStyle();
|
|
_currentCursorOptions.fUseColor = useColor;
|
|
_currentCursorOptions.cursorColor = cursorColor;
|
|
_currentCursorOptions.isVisible = _pData->IsCursorVisible();
|
|
_currentCursorOptions.isOn = _currentCursorOptions.isVisible && _pData->IsCursorOn();
|
|
_currentCursorOptions.inViewport = xInRange && yInRange;
|
|
}
|
|
|
|
void Renderer::_invalidateCurrentCursor() const
|
|
{
|
|
if (!_currentCursorOptions.inViewport || !_currentCursorOptions.isOn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto& buffer = _pData->GetTextBuffer();
|
|
const auto view = buffer.GetSize();
|
|
const auto coord = _currentCursorOptions.coordCursor;
|
|
|
|
const auto lineRendition = _currentCursorOptions.lineRendition;
|
|
const auto cursorWidth = _currentCursorOptions.fIsDoubleWidth ? 2 : 1;
|
|
const auto x = coord.x - _viewport.Left();
|
|
|
|
til::rect rect{ x, coord.y, x + cursorWidth, coord.y + 1 };
|
|
rect = BufferToScreenLine(rect, lineRendition);
|
|
|
|
if (view.TrimToViewport(&rect))
|
|
{
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateCursor(&rect));
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we had previously drawn a composition at the previous cursor position
|
|
// we need to invalidate the entire line because who knows what changed.
|
|
// (It's possible to figure that out, but not worth the effort right now.)
|
|
void Renderer::_invalidateOldComposition() const
|
|
{
|
|
if (!_compositionCache || !_currentCursorOptions.inViewport)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto& buffer = _pData->GetTextBuffer();
|
|
const auto view = buffer.GetSize();
|
|
const auto coord = _currentCursorOptions.coordCursor;
|
|
|
|
til::rect rect{ 0, coord.y, til::CoordTypeMax, coord.y + 1 };
|
|
if (view.TrimToViewport(&rect))
|
|
{
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->Invalidate(&rect));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Invalidate the line that the active TSF composition is on,
|
|
// so that _PaintBufferOutput() actually gets a chance to draw it.
|
|
void Renderer::_prepareNewComposition()
|
|
{
|
|
if (_pData->GetActiveComposition().text.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto viewport = _pData->GetViewport();
|
|
const auto coordCursor = _pData->GetCursorPosition();
|
|
|
|
til::rect line{ 0, coordCursor.y, til::CoordTypeMax, coordCursor.y + 1 };
|
|
if (viewport.TrimToViewport(&line))
|
|
{
|
|
viewport.ConvertToOrigin(&line);
|
|
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
LOG_IF_FAILED(pEngine->Invalidate(&line));
|
|
}
|
|
|
|
auto& buffer = _pData->GetTextBuffer();
|
|
auto& scratch = buffer.GetScratchpadRow();
|
|
const auto& activeComposition = _pData->GetActiveComposition();
|
|
|
|
std::wstring_view text{ activeComposition.text };
|
|
RowWriteState state{
|
|
.columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(),
|
|
};
|
|
|
|
state.text = text.substr(0, activeComposition.cursorPos);
|
|
scratch.ReplaceText(state);
|
|
const auto cursorOffset = state.columnEnd;
|
|
|
|
state.text = text.substr(activeComposition.cursorPos);
|
|
state.columnBegin = state.columnEnd;
|
|
scratch.ReplaceText(state);
|
|
|
|
// Ideally the text is inserted at the position of the cursor (`coordCursor`),
|
|
// but if we got more text than fits into the remaining space until the end of the line,
|
|
// then we'll insert the text aligned to the end of the line.
|
|
const auto remaining = state.columnLimit - state.columnEnd;
|
|
const auto beg = std::clamp(coordCursor.x, 0, remaining);
|
|
|
|
const auto baseAttribute = buffer.GetRowByOffset(coordCursor.y).GetAttrByColumn(coordCursor.x);
|
|
_compositionCache.emplace(til::point{ beg, coordCursor.y }, baseAttribute);
|
|
|
|
// Fake-move the cursor to where it needs to be in the active composition.
|
|
_currentCursorOptions.coordCursor.x = std::min(beg + cursorOffset, line.right - 1);
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paint helper to draw the cursor within the buffer.
|
|
// Arguments:
|
|
// - engine - The render engine that we're targeting.
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::_PaintCursor(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
if (_currentCursorOptions.inViewport && _currentCursorOptions.isVisible)
|
|
{
|
|
LOG_IF_FAILED(pEngine->PaintCursor(_currentCursorOptions));
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves info from the render data to prepare the engine with, before the
|
|
// frame is drawn. Some renderers might want to use this information to affect
|
|
// later drawing decisions.
|
|
// * Namely, the DX renderer uses this to know the cursor position and state
|
|
// before PaintCursor is called, so it can draw the cursor underneath the
|
|
// text.
|
|
// Arguments:
|
|
// - engine - The render engine that we're targeting.
|
|
// Return Value:
|
|
// - S_OK if the engine prepared successfully, or a relevant error via HRESULT.
|
|
[[nodiscard]] HRESULT Renderer::_PrepareRenderInfo(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
RenderFrameInfo info;
|
|
info.searchHighlights = _pData->GetSearchHighlights();
|
|
info.searchHighlightFocused = _pData->GetSearchHighlightFocused();
|
|
info.selectionSpans = _pData->GetSelectionSpans();
|
|
info.selectionBackground = _renderSettings.GetColorTableEntry(TextColor::SELECTION_BACKGROUND);
|
|
return pEngine->PrepareRenderInfo(std::move(info));
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paint helper to draw the selected area of the window.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
try
|
|
{
|
|
std::span<const til::rect> dirtyAreas;
|
|
LOG_IF_FAILED(pEngine->GetDirtyArea(dirtyAreas));
|
|
|
|
for (auto&& dirtyRect : dirtyAreas)
|
|
{
|
|
for (const auto& rect : _lastSelectionRectsByViewport)
|
|
{
|
|
if (const auto rectCopy{ rect & dirtyRect })
|
|
{
|
|
LOG_IF_FAILED(pEngine->PaintSelection(rectCopy));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Helper to convert the text attributes to actual RGB colors and update the rendering pen/brush within the rendering engine before the next draw operation.
|
|
// Arguments:
|
|
// - pEngine - Which engine is being updated
|
|
// - textAttributes - The 16 color foreground/background combination to set
|
|
// - usingSoftFont - Whether we're rendering characters from a soft font
|
|
// - isSettingDefaultBrushes - Alerts that the default brushes are being set which will
|
|
// impact whether or not to include the hung window/erase window brushes in this operation
|
|
// and can affect other draw state that wants to know the default color scheme.
|
|
// (Usually only happens when the default is changed, not when each individual color is swapped in a multi-color run.)
|
|
// Return Value:
|
|
// - <none>
|
|
[[nodiscard]] HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine,
|
|
const TextAttribute textAttributes,
|
|
const bool usingSoftFont,
|
|
const bool isSettingDefaultBrushes)
|
|
{
|
|
// The last color needs to be each engine's responsibility. If it's local to this function,
|
|
// then on the next engine we might not update the color.
|
|
return pEngine->UpdateDrawingBrushes(textAttributes, _renderSettings, _pData, usingSoftFont, isSettingDefaultBrushes);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Helper called before a majority of paint operations to scroll most of the previous frame into the appropriate
|
|
// position before we paint the remaining invalid area.
|
|
// - Used to save drawing time/improve performance
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
[[nodiscard]] HRESULT Renderer::_PerformScrolling(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
return pEngine->ScrollFrame();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Offsets all of the selection rectangles we might be holding onto
|
|
// as the previously selected area. If the whole viewport scrolls,
|
|
// we need to scroll these areas also to ensure they're invalidated
|
|
// properly when the selection further changes.
|
|
// Arguments:
|
|
// - delta - The scroll delta
|
|
// Return Value:
|
|
// - <none> - Updates internal state instead.
|
|
void Renderer::_ScrollPreviousSelection(const til::point delta)
|
|
{
|
|
if (delta != til::point{ 0, 0 })
|
|
{
|
|
for (auto& rc : _lastSelectionRectsByViewport)
|
|
{
|
|
rc += delta;
|
|
}
|
|
|
|
_currentCursorOptions.coordCursor += delta;
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Adds another Render engine to this renderer. Future rendering calls will
|
|
// also be sent to the new renderer.
|
|
// Arguments:
|
|
// - pEngine: The new render engine to be added
|
|
// Return Value:
|
|
// - <none>
|
|
// Throws if we ran out of memory or there was some other error appending the
|
|
// engine to our collection.
|
|
void Renderer::AddRenderEngine(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
THROW_HR_IF_NULL(E_INVALIDARG, pEngine);
|
|
|
|
for (auto& p : _engines)
|
|
{
|
|
if (!p)
|
|
{
|
|
p = pEngine;
|
|
_forceUpdateViewport = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
THROW_HR_MSG(E_UNEXPECTED, "engines array is full");
|
|
}
|
|
|
|
void Renderer::RemoveRenderEngine(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
THROW_HR_IF_NULL(E_INVALIDARG, pEngine);
|
|
|
|
for (auto& p : _engines)
|
|
{
|
|
if (p == pEngine)
|
|
{
|
|
p = nullptr;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Registers a callback for when the background color is changed
|
|
// Arguments:
|
|
// - pfn: the callback
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::SetBackgroundColorChangedCallback(std::function<void()> pfn)
|
|
{
|
|
_pfnBackgroundColorChanged = std::move(pfn);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Registers a callback for when the frame colors have changed
|
|
// Arguments:
|
|
// - pfn: the callback
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::SetFrameColorChangedCallback(std::function<void()> pfn)
|
|
{
|
|
_pfnFrameColorChanged = std::move(pfn);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Registers a callback that will be called when this renderer gives up.
|
|
// An application consuming a renderer can use this to display auxiliary Retry UI
|
|
// Arguments:
|
|
// - pfn: the callback
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::SetRendererEnteredErrorStateCallback(std::function<void()> pfn)
|
|
{
|
|
_pfnRendererEnteredErrorState = std::move(pfn);
|
|
}
|
|
|
|
void Renderer::UpdateHyperlinkHoveredId(uint16_t id) noexcept
|
|
{
|
|
_hyperlinkHoveredId = id;
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
pEngine->UpdateHyperlinkHoveredId(id);
|
|
}
|
|
}
|
|
|
|
void Renderer::UpdateLastHoveredInterval(const std::optional<PointTree::interval>& newInterval)
|
|
{
|
|
_hoveredInterval = newInterval;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Blocks until the engines are able to render without blocking.
|
|
void Renderer::WaitUntilCanRender()
|
|
{
|
|
FOREACH_ENGINE(pEngine)
|
|
{
|
|
pEngine->WaitUntilCanRender();
|
|
}
|
|
}
|