937 lines
33 KiB
C++
937 lines
33 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "renderer.hpp"
|
|
|
|
#pragma hdrstop
|
|
|
|
using namespace Microsoft::Console::Render;
|
|
using namespace Microsoft::Console::Types;
|
|
|
|
static constexpr auto maxRetriesForRenderEngine = 3;
|
|
|
|
// Routine Description:
|
|
// - Creates a new renderer controller for a console.
|
|
// Arguments:
|
|
// - pData - The interface to console data structures required for rendering
|
|
// - pEngine - The output engine for targeting each rendering frame
|
|
// Return Value:
|
|
// - An instance of a Renderer.
|
|
// NOTE: CAN THROW IF MEMORY ALLOCATION FAILS.
|
|
Renderer::Renderer(IRenderData* pData,
|
|
_In_reads_(cEngines) IRenderEngine** const rgpEngines,
|
|
const size_t cEngines,
|
|
std::unique_ptr<IRenderThread> thread) :
|
|
_pData(pData),
|
|
_pThread{ std::move(thread) },
|
|
_destructing{ false }
|
|
{
|
|
_srViewportPrevious = { 0 };
|
|
|
|
for (size_t i = 0; i < cEngines; i++)
|
|
{
|
|
IRenderEngine* engine = rgpEngines[i];
|
|
// NOTE: THIS CAN THROW IF MEMORY ALLOCATION FAILS.
|
|
AddRenderEngine(engine);
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Destroys an instance of a renderer
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
Renderer::~Renderer()
|
|
{
|
|
_destructing = true;
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
if (_destructing)
|
|
{
|
|
return S_FALSE;
|
|
}
|
|
|
|
for (IRenderEngine* const pEngine : _rgpEngines)
|
|
{
|
|
auto tries = maxRetriesForRenderEngine;
|
|
while (tries > 0)
|
|
{
|
|
const auto hr = _PaintFrameForEngine(pEngine);
|
|
if (E_PENDING == hr)
|
|
{
|
|
if (--tries == 0)
|
|
{
|
|
FAIL_FAST_HR_MSG(E_UNEXPECTED, "A rendering engine required too many retries.");
|
|
}
|
|
continue;
|
|
}
|
|
LOG_IF_FAILED(hr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
[[nodiscard]] HRESULT Renderer::_PaintFrameForEngine(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
FAIL_FAST_IF_NULL(pEngine); // This is a programming error. Fail fast.
|
|
|
|
_pData->LockConsole();
|
|
auto unlock = wil::scope_exit([&]() {
|
|
_pData->UnlockConsole();
|
|
});
|
|
|
|
// Last chance check if anything scrolled without an explicit invalidate notification since the last frame.
|
|
_CheckViewportAndScroll();
|
|
|
|
// Try to start painting a frame
|
|
HRESULT const 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());
|
|
});
|
|
|
|
// A. Prep Colors
|
|
RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), true));
|
|
|
|
// B. Perform Scroll Operations
|
|
RETURN_IF_FAILED(_PerformScrolling(pEngine));
|
|
|
|
// 1. Paint Background
|
|
RETURN_IF_FAILED(_PaintBackground(pEngine));
|
|
|
|
// 2. Paint Rows of Text
|
|
_PaintBufferOutput(pEngine);
|
|
|
|
// 3. Paint overlays that reside above the text buffer
|
|
_PaintOverlays(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();
|
|
|
|
// Force scope exit unlock to let go of global lock so other threads can run
|
|
unlock.reset();
|
|
|
|
// Trigger out-of-lock presentation for renderers that can support it
|
|
RETURN_IF_FAILED(pEngine->Present());
|
|
|
|
// As we leave the scope, EndPaint will be called (declared above)
|
|
return S_OK;
|
|
}
|
|
|
|
void Renderer::_NotifyPaintFrame()
|
|
{
|
|
// The thread will provide throttling for us.
|
|
_pThread->NotifyPaint();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when the system has requested we redraw a portion of the console.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerSystemRedraw(const RECT* const prcDirtyClient)
|
|
{
|
|
std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const 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)
|
|
{
|
|
Viewport view = _pData->GetViewport();
|
|
SMALL_RECT srUpdateRegion = region.ToExclusive();
|
|
|
|
if (view.TrimToViewport(&srUpdateRegion))
|
|
{
|
|
view.ConvertToOrigin(&srUpdateRegion);
|
|
std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const 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 COORD* const pcoord)
|
|
{
|
|
TriggerRedraw(Viewport::FromCoord(*pcoord)); // this will notify to paint if we need it.
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when the cursor has moved in the buffer. Allows for RenderEngines to
|
|
// differentiate between cursor movements and other invalidates.
|
|
// Visual Renderers (ex GDI) sohuld invalidate the position, while the VT
|
|
// engine ignores this. See MSFT:14711161.
|
|
// Arguments:
|
|
// - pcoord: The buffer-space position of the cursor.
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerRedrawCursor(const COORD* const pcoord)
|
|
{
|
|
Viewport view = _pData->GetViewport();
|
|
COORD updateCoord = *pcoord;
|
|
|
|
if (view.IsInBounds(updateCoord))
|
|
{
|
|
view.ConvertToOrigin(&updateCoord);
|
|
for (IRenderEngine* pEngine : _rgpEngines)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateCursor(&updateCoord));
|
|
|
|
// Double-wide cursors need to invalidate the right half as well.
|
|
if (_pData->IsCursorDoubleWidth())
|
|
{
|
|
updateCoord.X++;
|
|
LOG_IF_FAILED(pEngine->InvalidateCursor(&updateCoord));
|
|
}
|
|
}
|
|
|
|
_NotifyPaintFrame();
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerRedrawAll()
|
|
{
|
|
std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) {
|
|
LOG_IF_FAILED(pEngine->InvalidateAll());
|
|
});
|
|
|
|
_NotifyPaintFrame();
|
|
}
|
|
|
|
// 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()
|
|
{
|
|
// We need to shut down the paint thread on teardown.
|
|
_pThread->WaitForPaintCompletionAndDisable(INFINITE);
|
|
|
|
// Then walk through and do one final paint on the caller's thread.
|
|
for (IRenderEngine* const pEngine : _rgpEngines)
|
|
{
|
|
bool fEngineRequestsRepaint = false;
|
|
HRESULT hr = pEngine->PrepareForTeardown(&fEngineRequestsRepaint);
|
|
LOG_IF_FAILED(hr);
|
|
|
|
if (SUCCEEDED(hr) && fEngineRequestsRepaint)
|
|
{
|
|
LOG_IF_FAILED(_PaintFrameForEngine(pEngine));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when the selected area in the console has changed.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerSelection()
|
|
{
|
|
try
|
|
{
|
|
// Get selection rectangles
|
|
const auto rects = _GetSelectionRects();
|
|
|
|
std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) {
|
|
LOG_IF_FAILED(pEngine->InvalidateSelection(_previousSelection));
|
|
LOG_IF_FAILED(pEngine->InvalidateSelection(rects));
|
|
});
|
|
|
|
_previousSelection = rects;
|
|
|
|
_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()
|
|
{
|
|
SMALL_RECT const srOldViewport = _srViewportPrevious;
|
|
SMALL_RECT const srNewViewport = _pData->GetViewport().ToInclusive();
|
|
|
|
COORD coordDelta;
|
|
coordDelta.X = srOldViewport.Left - srNewViewport.Left;
|
|
coordDelta.Y = srOldViewport.Top - srNewViewport.Top;
|
|
|
|
std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) {
|
|
LOG_IF_FAILED(pEngine->UpdateViewport(srNewViewport));
|
|
LOG_IF_FAILED(pEngine->InvalidateScroll(&coordDelta));
|
|
});
|
|
_srViewportPrevious = srNewViewport;
|
|
|
|
return coordDelta.X != 0 || coordDelta.Y != 0;
|
|
}
|
|
|
|
// 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 COORD* const pcoordDelta)
|
|
{
|
|
std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) {
|
|
LOG_IF_FAILED(pEngine->InvalidateScroll(pcoordDelta));
|
|
});
|
|
|
|
_NotifyPaintFrame();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Called when the text buffer is about to circle its backing buffer.
|
|
// A renderer might want to get painted before that happens.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::TriggerCircling()
|
|
{
|
|
for (IRenderEngine* const pEngine : _rgpEngines)
|
|
{
|
|
bool fEngineRequestsRepaint = false;
|
|
HRESULT hr = pEngine->InvalidateCircling(&fEngineRequestsRepaint);
|
|
LOG_IF_FAILED(hr);
|
|
|
|
if (SUCCEEDED(hr) && fEngineRequestsRepaint)
|
|
{
|
|
LOG_IF_FAILED(_PaintFrameForEngine(pEngine));
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 std::wstring newTitle = _pData->GetConsoleTitle();
|
|
for (IRenderEngine* const pEngine : _rgpEngines)
|
|
{
|
|
LOG_IF_FAILED(pEngine->InvalidateTitle(newTitle));
|
|
}
|
|
_NotifyPaintFrame();
|
|
}
|
|
|
|
// 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 std::wstring 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)
|
|
{
|
|
std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) {
|
|
LOG_IF_FAILED(pEngine->UpdateDpi(iDpi));
|
|
LOG_IF_FAILED(pEngine->UpdateFont(FontInfoDesired, FontInfo));
|
|
});
|
|
|
|
_NotifyPaintFrame();
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
// If there's no head, return E_FAIL. The caller should decide how to
|
|
// handle this.
|
|
// Currently, the only caller is the WindowProc:WM_GETDPISCALEDSIZE handler.
|
|
// It will assume that the proposed font is 1x1, regardless of DPI.
|
|
if (_rgpEngines.size() < 1)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
|
|
// 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
|
|
FAIL_FAST_IF(!(_rgpEngines.size() <= 2));
|
|
for (IRenderEngine* const pEngine : _rgpEngines)
|
|
{
|
|
const HRESULT 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)
|
|
{
|
|
bool 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
|
|
FAIL_FAST_IF(!(_rgpEngines.size() <= 2));
|
|
for (IRenderEngine* const pEngine : _rgpEngines)
|
|
{
|
|
const HRESULT 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)
|
|
{
|
|
return fIsFullWidth;
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
_pThread->EnablePainting();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Waits for the current paint operation to complete, if any, up to the specified timeout.
|
|
// - Resets an event in the render thread that precludes it from advancing, thus disabling rendering.
|
|
// - If no paint operation is currently underway, returns immediately.
|
|
// Arguments:
|
|
// - dwTimeoutMs - Milliseconds to wait for the current paint operation to complete, if any (can be INFINITE).
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs)
|
|
{
|
|
_pThread->WaitForPaintCompletionAndDisable(dwTimeoutMs);
|
|
}
|
|
|
|
// 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 seperate 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();
|
|
|
|
// 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.
|
|
auto dirty = Viewport::FromInclusive(pEngine->GetDirtyRectInChars());
|
|
|
|
// 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);
|
|
|
|
// Shortcut: don't bother redrawing if the width is 0.
|
|
if (redraw.Width() > 0)
|
|
{
|
|
// Retrieve the text buffer so we can read information out of it.
|
|
const 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 bufferLine = Viewport::FromDimensions({ redraw.Left(), row }, { redraw.Width(), 1 });
|
|
|
|
// 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 1,1 because it is dirty but the viewport is actually looking
|
|
// at 13,26 relative to the buffer.
|
|
// This means that we need 14,27 out of the backing buffer to fill in the 1,1 cell of the screen.
|
|
const auto screenLine = Viewport::Offset(bufferLine, -view.Origin());
|
|
|
|
// Retrieve the cell information iterator limited to just this line we want to redraw.
|
|
auto it = buffer.GetCellDataAt(bufferLine.Origin(), bufferLine);
|
|
|
|
// Ask the helper to paint through this specific line.
|
|
_PaintBufferOutputHelper(pEngine, it, screenLine.Origin());
|
|
}
|
|
}
|
|
}
|
|
|
|
void Renderer::_PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine,
|
|
TextBufferCellIterator it,
|
|
const COORD target)
|
|
{
|
|
// 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.
|
|
std::vector<Cluster> clusters;
|
|
size_t cols = 0;
|
|
|
|
// Retrieve the first color.
|
|
auto color = it->TextAttr();
|
|
|
|
// 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;
|
|
|
|
// Update the drawing brushes with our color.
|
|
THROW_IF_FAILED(_UpdateDrawingBrushes(pEngine, currentRunColor, false));
|
|
|
|
// Advance the point by however many columns we've just outputted and reset the accumulator.
|
|
screenPoint.X += gsl::narrow<SHORT>(cols);
|
|
cols = 0;
|
|
|
|
// Ensure that our cluster vector is clear.
|
|
clusters.clear();
|
|
|
|
// This inner loop will accumulate clusters until the color changes.
|
|
// When the color changes, it will save the new color off and break.
|
|
do
|
|
{
|
|
if (color != it->TextAttr())
|
|
{
|
|
color = it->TextAttr();
|
|
break;
|
|
}
|
|
|
|
// Walk through the text data and turn it into rendering clusters.
|
|
clusters.emplace_back(it->Chars(), it->Columns());
|
|
|
|
// Advance the cluster and column counts.
|
|
const auto columnCount = clusters.back().GetColumns();
|
|
it += columnCount > 0 ? columnCount : 1; // prevent infinite loop for no visible columns
|
|
cols += columnCount;
|
|
|
|
} while (it);
|
|
|
|
// Do the painting.
|
|
// TODO: Calculate when trim left should be TRUE
|
|
THROW_IF_FAILED(pEngine->PaintBufferLine({ clusters.data(), clusters.size() }, screenPoint, false));
|
|
|
|
// If we're allowed to do grid drawing, draw that now too (since it will be coupled with the color data)
|
|
if (_pData->IsGridLineDrawingAllowed())
|
|
{
|
|
// We're only allowed to draw the grid lines under certain circumstances.
|
|
_PaintBufferOutputGridLineHelper(pEngine, currentRunColor, cols, screenPoint);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Generates a IRenderEngine::GridLines structure from the values in the
|
|
// provided textAttribute
|
|
// Arguments:
|
|
// - textAttribute: the TextAttribute to generate GridLines from.
|
|
// Return Value:
|
|
// - a GridLines containing all the gridline info from the TextAtribute
|
|
IRenderEngine::GridLines Renderer::s_GetGridlines(const TextAttribute& textAttribute) noexcept
|
|
{
|
|
// Convert console grid line representations into rendering engine enum representations.
|
|
IRenderEngine::GridLines lines = IRenderEngine::GridLines::None;
|
|
|
|
if (textAttribute.IsTopHorizontalDisplayed())
|
|
{
|
|
lines |= IRenderEngine::GridLines::Top;
|
|
}
|
|
|
|
if (textAttribute.IsBottomHorizontalDisplayed())
|
|
{
|
|
lines |= IRenderEngine::GridLines::Bottom;
|
|
}
|
|
|
|
if (textAttribute.IsLeftVerticalDisplayed())
|
|
{
|
|
lines |= IRenderEngine::GridLines::Left;
|
|
}
|
|
|
|
if (textAttribute.IsRightVerticalDisplayed())
|
|
{
|
|
lines |= IRenderEngine::GridLines::Right;
|
|
}
|
|
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 COORD coordTarget)
|
|
{
|
|
const COLORREF rgb = _pData->GetForegroundColor(textAttribute);
|
|
|
|
// Convert console grid line representations into rendering engine enum representations.
|
|
IRenderEngine::GridLines lines = Renderer::s_GetGridlines(textAttribute);
|
|
|
|
// Draw the lines
|
|
LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, rgb, cchLine, coordTarget));
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paint helper to draw the cursor within the buffer.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::_PaintCursor(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
if (_pData->IsCursorVisible())
|
|
{
|
|
// Get cursor position in buffer
|
|
COORD coordCursor = _pData->GetCursorPosition();
|
|
// Adjust cursor to viewport
|
|
Viewport view = _pData->GetViewport();
|
|
view.ConvertToOrigin(&coordCursor);
|
|
|
|
COLORREF cursorColor = _pData->GetCursorColor();
|
|
bool useColor = cursorColor != INVALID_COLOR;
|
|
|
|
// Build up the cursor parameters including position, color, and drawing options
|
|
IRenderEngine::CursorOptions options;
|
|
options.coordCursor = coordCursor;
|
|
options.ulCursorHeightPercent = _pData->GetCursorHeight();
|
|
options.cursorPixelWidth = _pData->GetCursorPixelWidth();
|
|
options.fIsDoubleWidth = _pData->IsCursorDoubleWidth();
|
|
options.cursorType = _pData->GetCursorStyle();
|
|
options.fUseColor = useColor;
|
|
options.cursorColor = cursorColor;
|
|
options.isOn = _pData->IsCursorOn();
|
|
|
|
// Draw it within the viewport
|
|
LOG_IF_FAILED(pEngine->PaintCursor(options));
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paint helper to draw text that overlays the main buffer to provide user interactivity regions
|
|
// - This supports IME composition.
|
|
// Arguments:
|
|
// - engine - The render engine that we're targeting.
|
|
// - overlay - The overlay to draw.
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::_PaintOverlay(IRenderEngine& engine,
|
|
const RenderOverlay& overlay)
|
|
{
|
|
try
|
|
{
|
|
// First get the screen buffer's viewport.
|
|
Viewport view = _pData->GetViewport();
|
|
|
|
// Now get the overlay's viewport and adjust it to where it is supposed to be relative to the window.
|
|
|
|
SMALL_RECT srCaView = overlay.region.ToInclusive();
|
|
srCaView.Top += overlay.origin.Y;
|
|
srCaView.Bottom += overlay.origin.Y;
|
|
srCaView.Left += overlay.origin.X;
|
|
srCaView.Right += overlay.origin.X;
|
|
|
|
// Set it up in a Viewport helper structure and trim it the IME viewport to be within the full console viewport.
|
|
Viewport viewConv = Viewport::FromInclusive(srCaView);
|
|
|
|
SMALL_RECT srDirty = engine.GetDirtyRectInChars();
|
|
|
|
// Dirty is an inclusive rectangle, but oddly enough the IME was an exclusive one, so correct it.
|
|
srDirty.Bottom++;
|
|
srDirty.Right++;
|
|
|
|
if (viewConv.TrimToViewport(&srDirty))
|
|
{
|
|
Viewport viewDirty = Viewport::FromInclusive(srDirty);
|
|
|
|
for (SHORT iRow = viewDirty.Top(); iRow < viewDirty.BottomInclusive(); iRow++)
|
|
{
|
|
const COORD target{ viewDirty.Left(), iRow };
|
|
const auto source = target - overlay.origin;
|
|
|
|
auto it = overlay.buffer.GetCellLineDataAt(source);
|
|
|
|
_PaintBufferOutputHelper(&engine, it, target);
|
|
}
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paint helper to draw the composition string portion of the IME.
|
|
// - This specifically is the string that appears at the cursor on the input line showing what the user is currently typing.
|
|
// - See also: Generic Paint IME helper method.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void Renderer::_PaintOverlays(_In_ IRenderEngine* const pEngine)
|
|
{
|
|
try
|
|
{
|
|
const auto overlays = _pData->GetOverlays();
|
|
|
|
for (const auto& overlay : overlays)
|
|
{
|
|
_PaintOverlay(*pEngine, overlay);
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
// 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
|
|
{
|
|
SMALL_RECT srDirty = pEngine->GetDirtyRectInChars();
|
|
Viewport dirtyView = Viewport::FromInclusive(srDirty);
|
|
|
|
// Get selection rectangles
|
|
const auto rectangles = _GetSelectionRects();
|
|
for (auto rect : rectangles)
|
|
{
|
|
if (dirtyView.TrimToViewport(&rect))
|
|
{
|
|
LOG_IF_FAILED(pEngine->PaintSelection(rect));
|
|
}
|
|
}
|
|
}
|
|
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
|
|
// - 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 isSettingDefaultBrushes)
|
|
{
|
|
const COLORREF rgbForeground = _pData->GetForegroundColor(textAttributes);
|
|
const COLORREF rgbBackground = _pData->GetBackgroundColor(textAttributes);
|
|
const WORD legacyAttributes = textAttributes.GetLegacyAttributes();
|
|
const auto extendedAttrs = textAttributes.GetExtendedAttributes();
|
|
|
|
// 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_IF_FAILED(pEngine->UpdateDrawingBrushes(rgbForeground, rgbBackground, legacyAttributes, extendedAttrs, isSettingDefaultBrushes));
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Helper to determine the selected region of the buffer.
|
|
// Return Value:
|
|
// - A vector of rectangles representing the regions to select, line by line.
|
|
std::vector<SMALL_RECT> Renderer::_GetSelectionRects() const
|
|
{
|
|
auto rects = _pData->GetSelectionRects();
|
|
// Adjust rectangles to viewport
|
|
Viewport view = _pData->GetViewport();
|
|
|
|
std::vector<SMALL_RECT> result;
|
|
|
|
for (auto& rect : rects)
|
|
{
|
|
auto sr = view.ConvertToOrigin(rect).ToInclusive();
|
|
|
|
// hopefully temporary, we should be receiving the right selection sizes without correction.
|
|
sr.Right++;
|
|
sr.Bottom++;
|
|
|
|
result.emplace_back(sr);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// 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_IF_NULL_ALLOC(pEngine);
|
|
_rgpEngines.push_back(pEngine);
|
|
}
|