diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 27724fa7cc9..2c9197b061f 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -471,7 +471,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _lastHoveredId = newId; _lastHoveredInterval = newInterval; _renderEngine->UpdateHyperlinkHoveredId(newId); - _renderer->UpdateLastHoveredInterval(newInterval); + _renderEngine->UpdateLastHoveredInterval(newInterval); _renderer->TriggerRedrawAll(); } diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index b1785380587..d99997b224d 100644 --- a/src/host/ut_host/VtRendererTests.cpp +++ b/src/host/ut_host/VtRendererTests.cpp @@ -240,7 +240,7 @@ void VtRendererTest::Xterm256TestInvalidate() Log::Comment(NoThrowString().Format( L"Make sure that scrolling only invalidates part of the viewport, and sends the right sequences")); COORD scrollDelta = { 0, 1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled one down, only top line is invalid. ----")); @@ -257,7 +257,7 @@ void VtRendererTest::Xterm256TestInvalidate() }); scrollDelta = { 0, 3 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( @@ -282,7 +282,7 @@ void VtRendererTest::Xterm256TestInvalidate() }); scrollDelta = { 0, -1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled one up, only bottom line is invalid. ----")); @@ -299,7 +299,7 @@ void VtRendererTest::Xterm256TestInvalidate() }); scrollDelta = { 0, -3 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled three up, only bottom 3 lines are invalid. ----")); @@ -327,9 +327,9 @@ void VtRendererTest::Xterm256TestInvalidate() L"Multiple scrolls are coalesced")); scrollDelta = { 0, 1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); scrollDelta = { 0, 2 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled three down, only top 3 lines are invalid. ----")); @@ -354,11 +354,11 @@ void VtRendererTest::Xterm256TestInvalidate() }); scrollDelta = { 0, 1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); Log::Comment(engine->_invalidMap.to_string().c_str()); scrollDelta = { 0, -1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); Log::Comment(engine->_invalidMap.to_string().c_str()); qExpectedInput.push_back("\x1b[2J"); @@ -903,7 +903,7 @@ void VtRendererTest::XtermTestInvalidate() Log::Comment(NoThrowString().Format( L"Make sure that scrolling only invalidates part of the viewport, and sends the right sequences")); COORD scrollDelta = { 0, 1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled one down, only top line is invalid. ----")); @@ -920,7 +920,7 @@ void VtRendererTest::XtermTestInvalidate() }); scrollDelta = { 0, 3 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled three down, only top 3 lines are invalid. ----")); @@ -944,7 +944,7 @@ void VtRendererTest::XtermTestInvalidate() }); scrollDelta = { 0, -1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled one up, only bottom line is invalid. ----")); @@ -961,7 +961,7 @@ void VtRendererTest::XtermTestInvalidate() }); scrollDelta = { 0, -3 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled three up, only bottom 3 lines are invalid. ----")); @@ -989,9 +989,9 @@ void VtRendererTest::XtermTestInvalidate() L"Multiple scrolls are coalesced")); scrollDelta = { 0, 1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); scrollDelta = { 0, 2 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled three down, only top 3 lines are invalid. ----")); @@ -1016,11 +1016,11 @@ void VtRendererTest::XtermTestInvalidate() }); scrollDelta = { 0, 1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); Log::Comment(engine->_invalidMap.to_string().c_str()); scrollDelta = { 0, -1 }; - VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); + VERIFY_SUCCEEDED(engine->TriggerScroll(&scrollDelta)); Log::Comment(engine->_invalidMap.to_string().c_str()); qExpectedInput.push_back("\x1b[2J"); @@ -1417,7 +1417,7 @@ void VtRendererTest::TestResize() // The renderer (in Renderer@_PaintFrameForEngine..._CheckViewportAndScroll) // will manually call UpdateViewport once before actually painting the // first frame. Replicate that behavior here - VERIFY_SUCCEEDED(engine->UpdateViewport(view.ToInclusive())); + engine->UpdateViewport(view.ToInclusive()); TestPaint(*engine, [&]() { VERIFY_IS_FALSE(engine->_firstPaint); @@ -1429,7 +1429,7 @@ void VtRendererTest::TestResize() const auto newView = Viewport::FromDimensions({ 0, 0 }, { 120, 30 }); qExpectedInput.push_back("\x1b[8;30;120t"); - VERIFY_SUCCEEDED(engine->UpdateViewport(newView.ToInclusive())); + engine->UpdateViewport(newView.ToInclusive()); TestPaint(*engine, [&]() { VERIFY_IS_TRUE(engine->_invalidMap.all()); diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index cf690ecd398..a79e9e63c32 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -114,11 +114,6 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid return S_FALSE; } -[[nodiscard]] HRESULT BgfxEngine::ScrollFrame() noexcept -{ - return S_OK; -} - [[nodiscard]] HRESULT BgfxEngine::PaintBackground() noexcept { PVOID OldRunBase; @@ -166,19 +161,6 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid CATCH_RETURN(); } -[[nodiscard]] HRESULT BgfxEngine::PaintBufferGridLines(GridLines const /*lines*/, - COLORREF const /*color*/, - size_t const /*cchLine*/, - COORD const /*coordTarget*/) noexcept -{ - return S_OK; -} - -[[nodiscard]] HRESULT BgfxEngine::PaintSelection(const SMALL_RECT /*rect*/) noexcept -{ - return S_OK; -} - [[nodiscard]] HRESULT BgfxEngine::PaintCursor(const CursorOptions& options) noexcept { // TODO: MSFT: 11448021 - Modify BGFX to support rendering full-width @@ -257,15 +239,3 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid *pResult = false; return S_OK; } - -// Method Description: -// - Updates the window's title string. -// Does nothing for BGFX. -// Arguments: -// - newTitle: the new string to use for the title of the window -// Return Value: -// - S_OK -[[nodiscard]] HRESULT BgfxEngine::_DoUpdateTitle(_In_ const std::wstring_view /*newTitle*/) noexcept -{ - return S_OK; -} diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 4fddcfb0b39..713255128bc 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -71,9 +71,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; - protected: - [[nodiscard]] HRESULT _DoUpdateTitle(_In_ const std::wstring_view newTitle) noexcept override; - private: ULONG_PTR _sharedViewBase; SIZE_T _runLength; diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index f31fff625d7..6552c5a58e7 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -3,9 +3,12 @@ #include "precomp.h" #include "../inc/RenderEngineBase.hpp" +#include "../../buffer/out/textBuffer.hpp" + #pragma hdrstop using namespace Microsoft::Console; using namespace Microsoft::Console::Render; +using namespace Microsoft::Console::Types; RenderEngineBase::RenderEngineBase() : _titleChanged(false), @@ -13,6 +16,13 @@ RenderEngineBase::RenderEngineBase() : { } +RenderEngineBase::RenderEngineBase(const Viewport initialViewport) : + _viewport(initialViewport), + _titleChanged(false), + _lastFrameTitle(L"") +{ +} + HRESULT RenderEngineBase::InvalidateTitle(const std::wstring_view proposedTitle) noexcept { if (proposedTitle != _lastFrameTitle) @@ -23,34 +33,51 @@ HRESULT RenderEngineBase::InvalidateTitle(const std::wstring_view proposedTitle) return S_OK; } -HRESULT RenderEngineBase::UpdateTitle(const std::wstring_view newTitle) noexcept +COORD RenderEngineBase::UpdateViewport(IRenderData* pData) noexcept { - HRESULT hr = S_FALSE; - if (newTitle != _lastFrameTitle) - { - RETURN_IF_FAILED(_DoUpdateTitle(newTitle)); - _lastFrameTitle = newTitle; - _titleChanged = false; - hr = S_OK; - } - return hr; -} + SMALL_RECT const srOldViewport = _viewport.ToInclusive(); + SMALL_RECT const srNewViewport = pData->GetViewport().ToInclusive(); -HRESULT RenderEngineBase::PrepareRenderInfo(const RenderFrameInfo& /*info*/) noexcept -{ - return S_FALSE; -} + COORD coordDelta; + coordDelta.X = srOldViewport.Left - srNewViewport.Left; + coordDelta.Y = srOldViewport.Top - srNewViewport.Top; -HRESULT RenderEngineBase::ResetLineTransform() noexcept -{ - return S_FALSE; + _viewport = Viewport::FromInclusive(srNewViewport); + + // If we're keeping some buffers between calls, let them know about the viewport size + // so they can prepare the buffers for changes to either preallocate memory at once + // (instead of growing naturally) or shrink down to reduce usage as appropriate. + const size_t lineLength = gsl::narrow_cast(til::rectangle{ srNewViewport }.width()); + til::manage_vector(_clusterBuffer, lineLength, _shrinkThreshold); + + return coordDelta; } -HRESULT RenderEngineBase::PrepareLineTransform(const LineRendition /*lineRendition*/, - const size_t /*targetRow*/, - const size_t /*viewportLeft*/) noexcept +bool RenderEngineBase::TriggerRedraw(IRenderData* pData, const Viewport& region) noexcept { - return S_FALSE; + Viewport view = _viewport; + SMALL_RECT 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); + LOG_IF_FAILED(Invalidate(&srUpdateRegion)); + return true; + } + + return false; } // Method Description: @@ -69,3 +96,397 @@ void RenderEngineBase::WaitUntilCanRender() noexcept { // do nothing by default } + +// 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: +// - +// Return Value: +// - nullopt if the cursor is off or out-of-frame, otherwise a CursorOptions +[[nodiscard]] std::optional RenderEngineBase::_GetCursorInfo(IRenderData* pData) +{ + if (pData->IsCursorVisible()) + { + // Get cursor position in buffer + COORD 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 view = ScreenToBufferLine(pData->GetViewport().ToInclusive(), 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; + if (xInRange && yInRange) + { + // Adjust cursor Y offset to viewport. + // The viewport X offset is saved in the options and handled with a transform. + coordCursor.Y -= view.Top; + + COLORREF cursorColor = pData->GetCursorColor(); + bool useColor = cursorColor != INVALID_COLOR; + + // Build up the cursor parameters including position, color, and drawing options + CursorOptions options; + options.coordCursor = coordCursor; + options.viewportLeft = pData->GetViewport().Left(); + options.lineRendition = lineRendition; + 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(); + + return { options }; + } + } + return std::nullopt; +} + +void RenderEngineBase::_LoopDirtyLines(IRenderData* pData, std::function action) +{ + // 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. + gsl::span dirtyAreas; + LOG_IF_FAILED(GetDirtyArea(dirtyAreas)); + + // Calling pData virtual functions is expensive. This won't change during painting. + const auto globalInvert = pData->IsScreenReversed(); + const auto gridLineDrawingAllowed = pData->IsGridLineDrawingAllowed(); + + for (const auto& dirtyRect : dirtyAreas) + { + auto dirty = Viewport::FromInclusive(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); + + // 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 screenLine = SMALL_RECT{ redraw.Left(), row, redraw.RightInclusive(), row }; + + // 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() - COORD{ 0, view.Top() }; + + // 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()); + + BufferLineRenderData renderData{ + pData, + buffer, + bufferLine.Origin(), + bufferLine, + screenPosition, + view, + lineRendition, + true, + lineWrapped, + globalInvert, + gridLineDrawingAllowed + }; + action(renderData); + } + } + } +} + +void RenderEngineBase::_LoopOverlay(IRenderData* pData, std::function action) +{ + // First get the screen buffer's viewport. + Viewport view = pData->GetViewport(); + const auto globalInvert = pData->IsScreenReversed(); + const auto gridLineDrawingAllowed = pData->IsGridLineDrawingAllowed(); + + // Now get the overlay's viewport and adjust it to where it is supposed to be relative to the window. + const auto overlays = pData->GetOverlays(); + for (const auto& overlay : overlays) + { + 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); + + gsl::span dirtyAreas; + LOG_IF_FAILED(GetDirtyArea(dirtyAreas)); + + for (SMALL_RECT srDirty : dirtyAreas) + { + // 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; + + SMALL_RECT limit; + limit.Top = source.Y; + limit.Bottom = source.Y; + limit.Left = 0; + limit.Right = overlay.buffer.GetSize().RightInclusive(); + + BufferLineRenderData renderData{ + pData, + overlay.buffer, + source, + Viewport::FromInclusive(limit), + target, + viewConv, + LineRendition::SingleWidth, + false, + false, + globalInvert, + gridLineDrawingAllowed + }; + action(renderData); + } + } + } + } +} + +void RenderEngineBase::_LoopSelection(IRenderData* pData, std::function action) +{ + gsl::span dirtyAreas; + LOG_IF_FAILED(GetDirtyArea(dirtyAreas)); + + // Get selection rectangles + const auto rectangles = _GetSelectionRects(pData); + for (auto rect : rectangles) + { + for (auto& dirtyRect : dirtyAreas) + { + // Make a copy as `TrimToViewport` will manipulate it and + // can destroy it for the next dirtyRect to test against. + auto rectCopy = rect; + Viewport dirtyView = Viewport::FromInclusive(dirtyRect); + if (dirtyView.TrimToViewport(&rectCopy)) + { + action(rectCopy); + } + } + } +} + +IRenderEngine::GridLines RenderEngineBase::_CalculateGridLines(IRenderData* pData, + const TextAttribute textAttribute, + const COORD coordTarget) +{ + // Convert console grid line representations into rendering engine enum representations. + IRenderEngine::GridLines lines = s_GetGridlines(textAttribute); + + // For now, we dash underline patterns and switch to regular underline on hover + // Since we're only rendering pattern links on *hover*, there's no point in checking + // the pattern range if we aren't currently hovering. + if (_hoveredInterval.has_value()) + { + const til::point coordTargetTil{ coordTarget }; + if (_hoveredInterval->start <= coordTargetTil && + coordTargetTil <= _hoveredInterval->stop) + { + if (pData->GetPatternId(coordTarget).size() > 0) + { + lines |= IRenderEngine::GridLines::Underline; + } + } + } + + return lines; +} + +// 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 TextAttribute +IRenderEngine::GridLines RenderEngineBase::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; + } + + if (textAttribute.IsCrossedOut()) + { + lines |= IRenderEngine::GridLines::Strikethrough; + } + + if (textAttribute.IsUnderlined()) + { + lines |= IRenderEngine::GridLines::Underline; + } + + if (textAttribute.IsDoublyUnderlined()) + { + lines |= IRenderEngine::GridLines::DoubleUnderline; + } + + if (textAttribute.IsHyperlink()) + { + lines |= IRenderEngine::GridLines::HyperlinkUnderline; + } + return lines; +} + +// 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 RenderEngineBase::_GetSelectionRects(IRenderData* pData) noexcept +{ + const auto& buffer = pData->GetTextBuffer(); + auto rects = pData->GetSelectionRects(); + // Adjust rectangles to viewport + Viewport view = pData->GetViewport(); + + std::vector result; + + for (auto rect : rects) + { + // Convert buffer offsets to the equivalent range of screen cells + // expected by callers, taking line rendition into account. + const auto lineRendition = buffer.GetLineRendition(rect.Top()); + rect = Viewport::FromInclusive(BufferToScreenLine(rect.ToInclusive(), lineRendition)); + + 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; +} + +std::vector RenderEngineBase::_CalculateCurrentSelection(IRenderData* pData) noexcept +{ + // Get selection rectangles + const auto rects = _GetSelectionRects(pData); + + // Restrict all previous selection rectangles to inside the current viewport bounds + for (auto& sr : _previousSelection) + { + // Make the exclusive SMALL_RECT into a til::rectangle. + til::rectangle rc{ Viewport::FromExclusive(sr).ToInclusive() }; + + // Make a viewport representing the coordinates that are currently presentable. + const til::rectangle viewport{ til::size{ pData->GetViewport().Dimensions() } }; + + // Intersect them so we only invalidate things that are still visible. + rc &= viewport; + + // Convert back into the exclusive SMALL_RECT and store in the vector. + sr = Viewport::FromInclusive(rc).ToExclusive(); + } + + return rects; +} + +// 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: +// - - Updates internal state instead. +void RenderEngineBase::_ScrollPreviousSelection(const til::point delta) +{ + if (delta != til::point{ 0, 0 }) + { + for (auto& sr : _previousSelection) + { + // Get a rectangle representing this piece of the selection. + til::rectangle rc = Viewport::FromExclusive(sr).ToInclusive(); + + // Offset the entire existing rectangle by the delta. + rc += delta; + + // Store it back into the vector. + sr = Viewport::FromInclusive(rc).ToExclusive(); + } + } +} diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index b53bc6eadf6..cee3c89bb4e 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -31,8 +31,7 @@ Renderer::Renderer(IRenderData* pData, _pData(THROW_HR_IF_NULL(E_INVALIDARG, pData)), _pThread{ std::move(thread) }, _destructing{ false }, - _clusterBuffer{}, - _viewport{ pData->GetViewport() } + _clusterBuffer{} { for (size_t i = 0; i < cEngines; i++) { @@ -143,32 +142,7 @@ try } }); - // A. Prep Colors - RETURN_IF_FAILED(_UpdateDrawingBrushes(pEngine, _pData->GetDefaultBrushColors(), 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); - - // 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)); + RETURN_IF_FAILED(pEngine->PaintFrame(_pData)); // Force scope exit end paint to finish up collecting information and possibly painting endPaint.reset(); @@ -217,30 +191,13 @@ void Renderer::TriggerSystemRedraw(const RECT* const prcDirtyClient) // - void Renderer::TriggerRedraw(const Viewport& region) { - Viewport view = _viewport; - SMALL_RECT 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)) + std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { + const bool needRedraw = pEngine->TriggerRedraw(_pData, region); + if (needRedraw) { - srUpdateRegion.Right *= 2; - break; + _NotifyPaintFrame(); } - } - - 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: @@ -348,32 +305,10 @@ void Renderer::TriggerSelection() { try { - // Get selection rectangles - const auto rects = _GetSelectionRects(); - - // Restrict all previous selection rectangles to inside the current viewport bounds - for (auto& sr : _previousSelection) - { - // Make the exclusive SMALL_RECT into a til::rectangle. - til::rectangle rc{ Viewport::FromExclusive(sr).ToInclusive() }; - - // Make a viewport representing the coordinates that are currently presentable. - const til::rectangle viewport{ til::size{ _pData->GetViewport().Dimensions() } }; - - // Intersect them so we only invalidate things that are still visible. - rc &= viewport; - - // Convert back into the exclusive SMALL_RECT and store in the vector. - sr = Viewport::FromInclusive(rc).ToExclusive(); - } - std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { - LOG_IF_FAILED(pEngine->InvalidateSelection(_previousSelection)); - LOG_IF_FAILED(pEngine->InvalidateSelection(rects)); + LOG_IF_FAILED(pEngine->TriggerSelection(_pData)); }); - _previousSelection = rects; - _NotifyPaintFrame(); } CATCH_LOG(); @@ -387,36 +322,13 @@ void Renderer::TriggerSelection() // - True if something changed and we scrolled. False otherwise. bool Renderer::_CheckViewportAndScroll() { - SMALL_RECT const srOldViewport = _viewport.ToInclusive(); - SMALL_RECT const srNewViewport = _pData->GetViewport().ToInclusive(); - - COORD coordDelta; - coordDelta.X = srOldViewport.Left - srNewViewport.Left; - coordDelta.Y = srOldViewport.Top - srNewViewport.Top; - for (auto engine : _rgpEngines) { - LOG_IF_FAILED(engine->UpdateViewport(srNewViewport)); - } - - _viewport = Viewport::FromInclusive(srNewViewport); - - // If we're keeping some buffers between calls, let them know about the viewport size - // so they can prepare the buffers for changes to either preallocate memory at once - // (instead of growing naturally) or shrink down to reduce usage as appropriate. - const size_t lineLength = gsl::narrow_cast(til::rectangle{ srNewViewport }.width()); - til::manage_vector(_clusterBuffer, lineLength, _shrinkThreshold); - - if (coordDelta.X != 0 || coordDelta.Y != 0) - { - for (auto engine : _rgpEngines) + COORD coordDelta = engine->UpdateViewport(_pData); + if (coordDelta.X != 0 || coordDelta.Y != 0) { - LOG_IF_FAILED(engine->InvalidateScroll(&coordDelta)); + LOG_IF_FAILED(engine->TriggerScroll(&coordDelta)); } - - _ScrollPreviousSelection(coordDelta); - - return true; } return false; @@ -449,11 +361,9 @@ void Renderer::TriggerScroll() void Renderer::TriggerScroll(const COORD* const pcoordDelta) { std::for_each(_rgpEngines.begin(), _rgpEngines.end(), [&](IRenderEngine* const pEngine) { - LOG_IF_FAILED(pEngine->InvalidateScroll(pcoordDelta)); + LOG_IF_FAILED(pEngine->TriggerScroll(pcoordDelta)); }); - _ScrollPreviousSelection(*pcoordDelta); - _NotifyPaintFrame(); } @@ -496,18 +406,6 @@ void Renderer::TriggerTitleChange() _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 auto newTitle = _pData->GetConsoleTitle(); - return pEngine->UpdateTitle(newTitle); -} - // Routine Description: // - Called when a change in font or DPI has been detected. // Arguments: @@ -620,654 +518,6 @@ 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: -// - -// Return Value: -// - -[[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: -// - -// Return Value: -// - -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. - gsl::span 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) - { - auto dirty = Viewport::FromInclusive(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); - - // 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 screenLine = SMALL_RECT{ redraw.Left(), row, redraw.RightInclusive(), row }; - - // 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() - COORD{ 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); - } - } - } -} - -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 COORD target, - const bool lineWrapped) -{ - auto globalInvert{ _pData->IsScreenReversed() }; - - // 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. - size_t cols = 0; - - // Retrieve the first color. - auto color = it->TextAttr(); - // Retrieve the first pattern id - auto patternIds = _pData->GetPatternId(target); - - // 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. - 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(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. - bool trimLeft = false; - - // Run contains wide character (>1 columns) - bool 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 - { - COORD thisPoint{ screenPoint.X + gsl::narrow(cols), screenPoint.Y }; - const auto thisPointPatterns = _pData->GetPatternId(thisPoint); - if (color != it->TextAttr() || patternIds != thisPointPatterns) - { - 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) || patternIds != thisPointPatterns) - { - color = newAttr; - patternIds = thisPointPatterns; - 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. - size_t columnCount = 0; - - // 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().IsTrailing()) - { - // 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 = it->Columns() + 1; - _clusterBuffer.emplace_back(it->Chars(), columnCount); - } - // Otherwise if it's not a special case, just insert it as is. - else - { - columnCount = it->Columns(); - _clusterBuffer.emplace_back(it->Chars(), columnCount); - } - - if (columnCount > 1) - { - containsWideCharacter = true; - } - - // Advance the cluster and column counts. - 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 (auto colsPainted = 0u; 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 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 TextAttribute -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; - } - - if (textAttribute.IsCrossedOut()) - { - lines |= IRenderEngine::GridLines::Strikethrough; - } - - if (textAttribute.IsUnderlined()) - { - lines |= IRenderEngine::GridLines::Underline; - } - - if (textAttribute.IsDoublyUnderlined()) - { - lines |= IRenderEngine::GridLines::DoubleUnderline; - } - - if (textAttribute.IsHyperlink()) - { - lines |= IRenderEngine::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: -// - -void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, - const TextAttribute textAttribute, - const size_t cchLine, - const COORD coordTarget) -{ - // Convert console grid line representations into rendering engine enum representations. - IRenderEngine::GridLines lines = Renderer::s_GetGridlines(textAttribute); - - // For now, we dash underline patterns and switch to regular underline on hover - // Since we're only rendering pattern links on *hover*, there's no point in checking - // the pattern range if we aren't currently hovering. - if (_hoveredInterval.has_value()) - { - const til::point coordTargetTil{ coordTarget }; - if (_hoveredInterval->start <= coordTargetTil && - coordTargetTil <= _hoveredInterval->stop) - { - if (_pData->GetPatternId(coordTarget).size() > 0) - { - lines |= IRenderEngine::GridLines::Underline; - } - } - } - - // Return early if there are no lines to paint. - if (lines != IRenderEngine::GridLines::None) - { - // Get the current foreground color to render the lines. - const COLORREF rgb = _pData->GetAttributeColors(textAttribute).first; - // Draw the lines - LOG_IF_FAILED(pEngine->PaintBufferGridLines(lines, rgb, cchLine, coordTarget)); - } -} - -// 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: -// - -// Return Value: -// - nullopt if the cursor is off or out-of-frame, otherwise a CursorOptions -[[nodiscard]] std::optional Renderer::_GetCursorInfo() -{ - if (_pData->IsCursorVisible()) - { - // Get cursor position in buffer - COORD 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 view = ScreenToBufferLine(_pData->GetViewport().ToInclusive(), 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; - if (xInRange && yInRange) - { - // Adjust cursor Y offset to viewport. - // The viewport X offset is saved in the options and handled with a transform. - coordCursor.Y -= view.Top; - - COLORREF cursorColor = _pData->GetCursorColor(); - bool useColor = cursorColor != INVALID_COLOR; - - // Build up the cursor parameters including position, color, and drawing options - CursorOptions options; - options.coordCursor = coordCursor; - options.viewportLeft = _pData->GetViewport().Left(); - options.lineRendition = lineRendition; - 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(); - - return { options }; - } - } - return std::nullopt; -} - -// Routine Description: -// - Paint helper to draw the cursor within the buffer. -// Arguments: -// - engine - The render engine that we're targeting. -// Return Value: -// - -void Renderer::_PaintCursor(_In_ IRenderEngine* const pEngine) -{ - const auto cursorInfo = _GetCursorInfo(); - if (cursorInfo.has_value()) - { - LOG_IF_FAILED(pEngine->PaintCursor(cursorInfo.value())); - } -} - -// 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.cursorInfo = _GetCursorInfo(); - return pEngine->PrepareRenderInfo(info); -} - -// 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: -// - -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); - - gsl::span dirtyAreas; - LOG_IF_FAILED(engine.GetDirtyArea(dirtyAreas)); - - for (SMALL_RECT srDirty : dirtyAreas) - { - // 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, false); - } - } - } - } - 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: -// - -// Return Value: -// - -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: -// - -// Return Value: -// - -void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) -{ - try - { - gsl::span dirtyAreas; - LOG_IF_FAILED(pEngine->GetDirtyArea(dirtyAreas)); - - // Get selection rectangles - const auto rectangles = _GetSelectionRects(); - for (auto rect : rectangles) - { - for (auto& dirtyRect : dirtyAreas) - { - // Make a copy as `TrimToViewport` will manipulate it and - // can destroy it for the next dirtyRect to test against. - auto rectCopy = rect; - Viewport dirtyView = Viewport::FromInclusive(dirtyRect); - if (dirtyView.TrimToViewport(&rectCopy)) - { - 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 -// - 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: -// - -[[nodiscard]] HRESULT Renderer::_UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute textAttributes, 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, _pData, 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: -// - -// Return Value: -// - -[[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 Renderer::_GetSelectionRects() const -{ - const auto& buffer = _pData->GetTextBuffer(); - auto rects = _pData->GetSelectionRects(); - // Adjust rectangles to viewport - Viewport view = _pData->GetViewport(); - - std::vector result; - - for (auto rect : rects) - { - // Convert buffer offsets to the equivalent range of screen cells - // expected by callers, taking line rendition into account. - const auto lineRendition = buffer.GetLineRendition(rect.Top()); - rect = Viewport::FromInclusive(BufferToScreenLine(rect.ToInclusive(), lineRendition)); - - 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: -// - 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: -// - - Updates internal state instead. -void Renderer::_ScrollPreviousSelection(const til::point delta) -{ - if (delta != til::point{ 0, 0 }) - { - for (auto& sr : _previousSelection) - { - // Get a rectangle representing this piece of the selection. - til::rectangle rc = Viewport::FromExclusive(sr).ToInclusive(); - - // Offset the entire existing rectangle by the delta. - rc += delta; - - // Store it back into the vector. - sr = Viewport::FromInclusive(rc).ToExclusive(); - } - } -} - // Method Description: // - Adds another Render engine to this renderer. Future rendering calls will // also be sent to the new renderer. @@ -1280,6 +530,7 @@ void Renderer::_ScrollPreviousSelection(const til::point delta) void Renderer::AddRenderEngine(_In_ IRenderEngine* const pEngine) { THROW_HR_IF_NULL(E_INVALIDARG, pEngine); + pEngine->UpdateViewport(_pData); _rgpEngines.push_back(pEngine); } @@ -1303,11 +554,6 @@ void Renderer::ResetErrorStateAndResume() EnablePainting(); } -void Renderer::UpdateLastHoveredInterval(const std::optional& newInterval) -{ - _hoveredInterval = newInterval; -} - // Method Description: // - Blocks until the engines are able to render without blocking. void Renderer::WaitUntilCanRender() diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 6f8f6d7a8c4..6f6cdae664f 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -80,8 +80,6 @@ namespace Microsoft::Console::Render void SetRendererEnteredErrorStateCallback(std::function pfn); void ResetErrorStateAndResume(); - void UpdateLastHoveredInterval(const std::optional::interval>& newInterval); - private: std::deque _rgpEngines; @@ -90,54 +88,16 @@ namespace Microsoft::Console::Render std::unique_ptr _pThread; bool _destructing = false; - std::optional::interval> _hoveredInterval; - void _NotifyPaintFrame(); [[nodiscard]] HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept; bool _CheckViewportAndScroll(); - [[nodiscard]] HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine); - - void _PaintBufferOutput(_In_ IRenderEngine* const pEngine); - - void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, - TextBufferCellIterator it, - const COORD target, - const bool lineWrapped); - - static IRenderEngine::GridLines s_GetGridlines(const TextAttribute& textAttribute) noexcept; - - void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, - const TextAttribute textAttribute, - const size_t cchLine, - const COORD coordTarget); - - void _PaintSelection(_In_ IRenderEngine* const pEngine); - void _PaintCursor(_In_ IRenderEngine* const pEngine); - - void _PaintOverlays(_In_ IRenderEngine* const pEngine); - void _PaintOverlay(IRenderEngine& engine, const RenderOverlay& overlay); - - [[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute attr, const bool isSettingDefaultBrushes); - - [[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine); - - Microsoft::Console::Types::Viewport _viewport; - - static constexpr float _shrinkThreshold = 0.8f; std::vector _clusterBuffer; - std::vector _GetSelectionRects() const; - void _ScrollPreviousSelection(const til::point delta); std::vector _previousSelection; - [[nodiscard]] HRESULT _PaintTitle(IRenderEngine* const pEngine); - - [[nodiscard]] std::optional _GetCursorInfo(); - [[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine); - // Helper functions to diagnose issues with painting and layout. // These are only actually effective/on in Debug builds when the flag is set using an attached debugger. bool _fDebug = false; diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 03904814e2b..75d5c0a5ff6 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -8,6 +8,7 @@ #include "../../interactivity/win32/CustomWindowMessages.h" #include "../../types/inc/Viewport.hpp" +#include "../../buffer/out/textBuffer.hpp" #include "../../inc/unicode.hpp" #include "../../inc/DefaultSettings.h" #include @@ -1121,17 +1122,28 @@ CATCH_RETURN(); // - rectangles - One or more rectangles describing character positions on the grid // Return Value: // - S_OK -[[nodiscard]] HRESULT DxEngine::InvalidateSelection(const std::vector& rectangles) noexcept +[[nodiscard]] HRESULT DxEngine::TriggerSelection(IRenderData* pData) noexcept +try { + const auto rects = _CalculateCurrentSelection(pData); if (!_allInvalid) { - for (const auto& rect : rectangles) + for (const auto& rect : _previousSelection) + { + RETURN_IF_FAILED(Invalidate(&rect)); + } + + for (const auto& rect : rects) { RETURN_IF_FAILED(Invalidate(&rect)); } } + + _previousSelection = rects; + return S_OK; } +CATCH_RETURN(); // Routine Description: // - Scrolls the existing dirty region (if it exists) and @@ -1141,7 +1153,7 @@ CATCH_RETURN(); // - -Y is up, Y is down, -X is left, X is right. // Return Value: // - S_OK -[[nodiscard]] HRESULT DxEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept +[[nodiscard]] HRESULT DxEngine::TriggerScroll(const COORD* const pcoordDelta) noexcept try { RETURN_HR_IF(E_INVALIDARG, !pcoordDelta); @@ -1159,6 +1171,8 @@ try } } + _ScrollPreviousSelection(*pcoordDelta); + return S_OK; } CATCH_RETURN(); @@ -1352,6 +1366,47 @@ try } CATCH_RETURN() +// Routine description: +// - Actual painting procedure +// Arguments: +// - +// Return Value: +// - Any DirectX error, a memory error, etc. +[[nodiscard]] HRESULT DxEngine::PaintFrame(IRenderData* pData) noexcept +try +{ + if (pData == nullptr) + { + return S_OK; + } + + // Prep Colors + RETURN_IF_FAILED(UpdateDrawingBrushes(pData->GetDefaultBrushColors(), pData, true)); + + // Prepare the engine with additional information before we start drawing. + RenderFrameInfo info; + info.cursorInfo = _GetCursorInfo(pData); + RETURN_IF_FAILED(PrepareRenderInfo(info)); + + // Paint Background + RETURN_IF_FAILED(PaintBackground()); + + // Paint Rows of Text + _LoopDirtyLines(pData, std::bind(&DxEngine::_PaintBufferLineHelper, this, std::placeholders::_1)); + + // Paint overlays that reside above the text buffer + _LoopOverlay(pData, std::bind(&DxEngine::_PaintBufferLineHelper, this, std::placeholders::_1)); + + // Paint Selection + _LoopSelection(pData, std::bind(&DxEngine::PaintSelection, this, std::placeholders::_1)); + + // Paint window title + RETURN_IF_FAILED(_UpdateTitle(pData->GetConsoleTitle())); + + return S_OK; +} +CATCH_RETURN(); + // Routine Description: // - Ends batch drawing and captures any state necessary for presentation // Arguments: @@ -1601,17 +1656,6 @@ void DxEngine::WaitUntilCanRender() noexcept return S_OK; } -// Routine Description: -// - This is currently unused. -// Arguments: -// - -// Return Value: -// - S_OK -[[nodiscard]] HRESULT DxEngine::ScrollFrame() noexcept -{ - return S_OK; -} - // Routine Description: // - This paints in the back most layer of the frame with the background color. // Arguments: @@ -1833,18 +1877,6 @@ try } CATCH_RETURN() -// Routine Description: -// - Does nothing. Our cursor is drawn in CustomTextRenderer::DrawGlyphRun, -// either above or below the text. -// Arguments: -// - options - unused -// Return Value: -// - S_OK -[[nodiscard]] HRESULT DxEngine::PaintCursor(const CursorOptions& /*options*/) noexcept -{ - return S_OK; -} - // Routine Description: // - Paint terminal effects. // Arguments: @@ -2044,18 +2076,6 @@ float DxEngine::GetScaling() const noexcept return _scale; } -// Method Description: -// - This method will update our internal reference for how big the viewport is. -// Does nothing for DX. -// Arguments: -// - srNewViewport - The bounds of the new viewport. -// Return Value: -// - HRESULT S_OK -[[nodiscard]] HRESULT DxEngine::UpdateViewport(const SMALL_RECT /*srNewViewport*/) noexcept -{ - return S_OK; -} - // Routine Description: // - Currently unused by this renderer // Arguments: @@ -2132,13 +2152,193 @@ CATCH_RETURN(); // - newTitle: the new string to use for the title of the window // Return Value: // - S_OK -[[nodiscard]] HRESULT DxEngine::_DoUpdateTitle(_In_ const std::wstring_view /*newTitle*/) noexcept +[[nodiscard]] HRESULT DxEngine::_UpdateTitle(const std::wstring_view newTitle) noexcept +try { - if (_hwndTarget != INVALID_HANDLE_VALUE) + HRESULT hr = S_FALSE; + if (newTitle != _lastFrameTitle) { - return PostMessageW(_hwndTarget, CM_UPDATE_TITLE, 0, 0) ? S_OK : E_FAIL; + if (_hwndTarget != INVALID_HANDLE_VALUE) + { + return PostMessageW(_hwndTarget, CM_UPDATE_TITLE, 0, 0) ? S_OK : E_FAIL; + } + + _lastFrameTitle = newTitle; + _titleChanged = false; + hr = S_OK; + } + return hr; +} +CATCH_RETURN(); + +void DxEngine::_PaintBufferLineHelper(const BufferLineRenderData& renderData) +{ + // Retrieve the cell information iterator limited to just this line we want to redraw. + auto it = renderData.buffer.GetCellDataAt(renderData.bufferPosition, renderData.bufferLimit); + if (!it) + { + return; + } + auto pData = renderData.pData; + + // If we have valid data, let's figure out how to draw 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. + size_t cols = 0; + + // Retrieve the first color. + auto color = it->TextAttr(); + // Retrieve the first pattern id + auto patternIds = pData->GetPatternId(renderData.screenPosition); + + // And hold the point where we should start drawing. + auto screenPoint = renderData.screenPosition; + + // 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. + THROW_IF_FAILED(UpdateDrawingBrushes(currentRunColor, pData, false)); + + // Advance the point by however many columns we've just outputted and reset the accumulator. + screenPoint.X += gsl::narrow(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. + bool trimLeft = false; + + // Run contains wide character (>1 columns) + bool 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 + { + COORD thisPoint{ screenPoint.X + gsl::narrow(cols), screenPoint.Y }; + const auto thisPointPatterns = pData->GetPatternId(thisPoint); + if (color != it->TextAttr() || patternIds != thisPointPatterns) + { + const 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 (!s_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, renderData.globalInvert) || patternIds != thisPointPatterns) + { + color = newAttr; + patternIds = thisPointPatterns; + 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. + size_t columnCount = 0; + + // 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().IsTrailing()) + { + // 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 = it->Columns() + 1; + _clusterBuffer.emplace_back(it->Chars(), columnCount); + } + // Otherwise if it's not a special case, just insert it as is. + else + { + columnCount = it->Columns(); + _clusterBuffer.emplace_back(it->Chars(), columnCount); + } + + if (columnCount > 1) + { + containsWideCharacter = true; + } + + // Advance the cluster and column counts. + it += std::max(it->Columns(), 1); // prevent infinite loop for no visible columns + cols += columnCount; + + } while (it); + + // Do the painting. + THROW_IF_FAILED(PaintBufferLine({ _clusterBuffer.data(), _clusterBuffer.size() }, screenPoint, trimLeft, renderData.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 (renderData.gridLineDrawingAllowed) + { + // 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 (auto colsPainted = 0u; colsPainted < cols; ++colsPainted, ++lineIt, ++lineTarget.X) + { + const auto lines = lineIt->TextAttr(); + const auto gridLines = _CalculateGridLines(pData, lines, lineTarget); + // Return early if there are no lines to paint. + if (gridLines != IRenderEngine::GridLines::None) + { + // Get the current foreground color to render the lines. + const COLORREF rgb = pData->GetAttributeColors(lines).first; + // Draw the lines + LOG_IF_FAILED(PaintBufferGridLines(gridLines, rgb, 1, lineTarget)); + } + } + } + else + { + // If nothing exciting is going on, draw the lines in bulk. + const auto gridLines = _CalculateGridLines(pData, currentRunColor, screenPoint); + // Return early if there are no lines to paint. + if (gridLines != IRenderEngine::GridLines::None) + { + // Get the current foreground color to render the lines. + const COLORREF rgb = pData->GetAttributeColors(currentRunColor).first; + // Draw the lines + LOG_IF_FAILED(PaintBufferGridLines(gridLines, rgb, cols, screenPoint)); + } + } + } } - return S_FALSE; } // Routine Description: diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index cbd34ea3b78..ff53cb1e705 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -76,13 +76,14 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; [[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override; [[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; - [[nodiscard]] HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; - [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + [[nodiscard]] HRESULT TriggerSelection(IRenderData* pData) noexcept override; + [[nodiscard]] HRESULT TriggerScroll(const COORD* const pcoordDelta) noexcept override; [[nodiscard]] HRESULT InvalidateAll() noexcept override; [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override; [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; [[nodiscard]] HRESULT StartPaint() noexcept override; + [[nodiscard]] HRESULT PaintFrame(IRenderData* pData) noexcept override; [[nodiscard]] HRESULT EndPaint() noexcept override; [[nodiscard]] bool RequiresContinuousRedraw() noexcept override; @@ -90,27 +91,22 @@ namespace Microsoft::Console::Render void WaitUntilCanRender() noexcept override; [[nodiscard]] HRESULT Present() noexcept override; - [[nodiscard]] HRESULT ScrollFrame() noexcept override; + [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept; - [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; - - [[nodiscard]] HRESULT PaintBackground() noexcept override; + [[nodiscard]] HRESULT PaintBackground() noexcept; [[nodiscard]] HRESULT PaintBufferLine(gsl::span const clusters, COORD const coord, bool const fTrimLeft, - const bool lineWrapped) noexcept override; - - [[nodiscard]] HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; - [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; + const bool lineWrapped) noexcept; - [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; + [[nodiscard]] HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept; + [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept; [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, - const bool isSettingDefaultBrushes) noexcept override; + const bool isSettingDefaultBrushes) noexcept; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; - [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; @@ -131,7 +127,8 @@ namespace Microsoft::Console::Render void UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept; protected: - [[nodiscard]] HRESULT _DoUpdateTitle(_In_ const std::wstring_view newTitle) noexcept override; + void _PaintBufferLineHelper(const BufferLineRenderData& renderData); + [[nodiscard]] HRESULT _UpdateTitle(const std::wstring_view newTitle) noexcept; [[nodiscard]] HRESULT _PaintTerminalEffects() noexcept; [[nodiscard]] bool _FullRepaintNeeded() const noexcept; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 230b09c4f6c..c58de087baa 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -26,8 +26,8 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT SetHwnd(const HWND hwnd) noexcept; - [[nodiscard]] HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; - [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + [[nodiscard]] HRESULT TriggerSelection(IRenderData* pData) noexcept override; + [[nodiscard]] HRESULT TriggerScroll(const COORD* const pcoordDelta) noexcept override; [[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; [[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override; @@ -36,36 +36,36 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; [[nodiscard]] HRESULT StartPaint() noexcept override; + [[nodiscard]] HRESULT PaintFrame(IRenderData* pData) noexcept override; [[nodiscard]] HRESULT EndPaint() noexcept override; [[nodiscard]] HRESULT Present() noexcept override; - [[nodiscard]] HRESULT ScrollFrame() noexcept override; + [[nodiscard]] HRESULT ScrollFrame() noexcept; - [[nodiscard]] HRESULT ResetLineTransform() noexcept override; + [[nodiscard]] HRESULT ResetLineTransform() noexcept; [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, const size_t targetRow, - const size_t viewportLeft) noexcept override; + const size_t viewportLeft) noexcept; - [[nodiscard]] HRESULT PaintBackground() noexcept override; + [[nodiscard]] HRESULT PaintBackground() noexcept; [[nodiscard]] HRESULT PaintBufferLine(gsl::span const clusters, const COORD coord, const bool trimLeft, - const bool lineWrapped) noexcept override; + const bool lineWrapped) noexcept; [[nodiscard]] HRESULT PaintBufferGridLines(const GridLines lines, const COLORREF color, const size_t cchLine, - const COORD coordTarget) noexcept override; - [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; + const COORD coordTarget) noexcept; + [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept; - [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; + [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept; [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, - const bool isSettingDefaultBrushes) noexcept override; + const bool isSettingDefaultBrushes) noexcept; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override; - [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& FontDesired, _Out_ FontInfo& Font, @@ -76,7 +76,9 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; protected: - [[nodiscard]] HRESULT _DoUpdateTitle(_In_ const std::wstring_view newTitle) noexcept override; + void GdiEngine::_PaintBufferLineHelper(const BufferLineRenderData& renderData); + + [[nodiscard]] HRESULT _UpdateTitle(const std::wstring_view newTitle) noexcept; private: HWND _hwndTargetWindow; diff --git a/src/renderer/gdi/invalidate.cpp b/src/renderer/gdi/invalidate.cpp index ca77b03cd12..67dbfa4bc62 100644 --- a/src/renderer/gdi/invalidate.cpp +++ b/src/renderer/gdi/invalidate.cpp @@ -28,7 +28,7 @@ HRESULT GdiEngine::InvalidateSystem(const RECT* const prcDirtyClient) noexcept // - pcoordDelta - Pointer to character dimension (COORD) of the distance the console would like us to move while scrolling. // Return Value: // - HRESULT S_OK, GDI-based error code, or safemath error -HRESULT GdiEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept +HRESULT GdiEngine::TriggerScroll(const COORD* const pcoordDelta) noexcept { if (pcoordDelta->X != 0 || pcoordDelta->Y != 0) { @@ -45,6 +45,8 @@ HRESULT GdiEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept _szInvalidScroll = szInvalidScrollNew; } + _ScrollPreviousSelection(*pcoordDelta); + return S_OK; } @@ -54,13 +56,22 @@ HRESULT GdiEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept // - rectangles - Vector of rectangles to draw, line by line // Return Value: // - HRESULT S_OK or GDI-based error code -HRESULT GdiEngine::InvalidateSelection(const std::vector& rectangles) noexcept +HRESULT GdiEngine::TriggerSelection(IRenderData* pData) noexcept { + const auto rectangles = _CalculateCurrentSelection(pData); + + for (const auto& rect : _previousSelection) + { + RETURN_IF_FAILED(Invalidate(&rect)); + } + for (const auto& rect : rectangles) { RETURN_IF_FAILED(Invalidate(&rect)); } + _previousSelection = rectangles; + return S_OK; } diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 089e9777e69..f5582b6d228 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -57,6 +57,44 @@ using namespace Microsoft::Console::Render; return S_OK; } +[[nodiscard]] HRESULT GdiEngine::PaintFrame(IRenderData* pData) noexcept +{ + // Prep Colors + RETURN_IF_FAILED(UpdateDrawingBrushes(pData->GetDefaultBrushColors(), pData, true)); + + // Perform Scroll Operations + RETURN_IF_FAILED(ScrollFrame()); + + // Paint Background + RETURN_IF_FAILED(PaintBackground()); + + // This is to make sure any transforms are reset when this paint is finished. + auto resetLineTransform = wil::scope_exit([&]() { + LOG_IF_FAILED(ResetLineTransform()); + }); + + // Paint Rows of Text + _LoopDirtyLines(pData, std::bind(&GdiEngine::_PaintBufferLineHelper, this, std::placeholders::_1)); + + // Paint overlays that reside above the text buffer + _LoopOverlay(pData, std::bind(&GdiEngine::_PaintBufferLineHelper, this, std::placeholders::_1)); + + // Paint Selection + _LoopSelection(pData, std::bind(&GdiEngine::PaintSelection, this, std::placeholders::_1)); + + // Paint Cursor + const auto cursorInfo = _GetCursorInfo(pData); + if (cursorInfo.has_value()) + { + LOG_IF_FAILED(PaintCursor(cursorInfo.value())); + } + + // Paint window title + RETURN_IF_FAILED(_UpdateTitle(pData->GetConsoleTitle())); + + return S_OK; +} + // Routine Description: // - Scrolls the existing data on the in-memory frame by the scroll region // deltas we have collectively received through the Invalidate methods @@ -664,6 +702,7 @@ using namespace Microsoft::Console::Render; // Prepare the appropriate line transform for the current row. LOG_IF_FAILED(PrepareLineTransform(options.lineRendition, 0, options.viewportLeft)); + auto resetLineTransform = wil::scope_exit([&]() { LOG_IF_FAILED(ResetLineTransform()); }); diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index 3311e75858e..8f979d4b58c 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -5,6 +5,7 @@ #include "gdirenderer.hpp" #include "../../inc/conattrs.hpp" +#include "../../buffer/out/textBuffer.hpp" #include // for GWL_CONSOLE_BKCOLOR #include "../../interactivity/win32/CustomWindowMessages.h" #pragma hdrstop @@ -436,18 +437,6 @@ GdiEngine::~GdiEngine() return S_OK; } -// Method Description: -// - This method will update our internal reference for how big the viewport is. -// Does nothing for GDI. -// Arguments: -// - srNewViewport - The bounds of the new viewport. -// Return Value: -// - HRESULT S_OK -[[nodiscard]] HRESULT GdiEngine::UpdateViewport(const SMALL_RECT /*srNewViewport*/) noexcept -{ - return S_OK; -} - // Routine Description: // - This method will figure out what the new font should be given the starting font information and a DPI. // - When the final font is determined, the FontInfo structure given will be updated with the actual resulting font chosen as the nearest match. @@ -465,17 +454,191 @@ GdiEngine::~GdiEngine() return _GetProposedFont(FontDesired, Font, iDpi, hFont, hFontItalic); } +void GdiEngine::_PaintBufferLineHelper(const BufferLineRenderData& renderData) +{ + if (renderData.needLineTransformation) + { + LOG_IF_FAILED(PrepareLineTransform(renderData.lineRendition, renderData.screenPosition.Y, renderData.visible.Left())); + } + + // Retrieve the cell information iterator limited to just this line we want to redraw. + auto it = renderData.buffer.GetCellDataAt(renderData.bufferPosition, renderData.bufferLimit); + if (!it) + { + return; + } + auto pData = renderData.pData; + + // If we have valid data, let's figure out how to draw 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. + size_t cols = 0; + + // Retrieve the first color. + auto color = it->TextAttr(); + + // And hold the point where we should start drawing. + auto screenPoint = renderData.screenPosition; + + // 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(currentRunColor, pData, false)); + + // Advance the point by however many columns we've just outputted and reset the accumulator. + screenPoint.X += gsl::narrow(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. + bool trimLeft = false; + + // Run contains wide character (>1 columns) + bool 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 + { + COORD thisPoint{ screenPoint.X + gsl::narrow(cols), screenPoint.Y }; + if (color != it->TextAttr()) + { + 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 (!s_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, renderData.globalInvert)) + { + color = newAttr; + 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. + size_t columnCount = 0; + + // 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().IsTrailing()) + { + // 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 = it->Columns() + 1; + _clusterBuffer.emplace_back(it->Chars(), columnCount); + } + // Otherwise if it's not a special case, just insert it as is. + else + { + columnCount = it->Columns(); + _clusterBuffer.emplace_back(it->Chars(), columnCount); + } + + if (columnCount > 1) + { + containsWideCharacter = true; + } + + // Advance the cluster and column counts. + it += std::max(it->Columns(), 1); // prevent infinite loop for no visible columns + cols += columnCount; + + } while (it); + + // Do the painting. + THROW_IF_FAILED(PaintBufferLine({ _clusterBuffer.data(), _clusterBuffer.size() }, screenPoint, trimLeft, renderData.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 (renderData.gridLineDrawingAllowed) + { + // 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 (auto colsPainted = 0u; colsPainted < cols; ++colsPainted, ++lineIt, ++lineTarget.X) + { + auto lines = lineIt->TextAttr(); + auto gridLines = _CalculateGridLines(pData, lines, lineTarget); + // Return early if there are no lines to paint. + if (gridLines != IRenderEngine::GridLines::None) + { + // Get the current foreground color to render the lines. + const COLORREF rgb = pData->GetAttributeColors(lines).first; + // Draw the lines + LOG_IF_FAILED(PaintBufferGridLines(gridLines, rgb, 1, lineTarget)); + } + } + } + else + { + // If nothing exciting is going on, draw the lines in bulk. + auto gridLines = _CalculateGridLines(pData, currentRunColor, screenPoint); + // Return early if there are no lines to paint. + if (gridLines != IRenderEngine::GridLines::None) + { + // Get the current foreground color to render the lines. + const COLORREF rgb = pData->GetAttributeColors(currentRunColor).first; + // Draw the lines + LOG_IF_FAILED(PaintBufferGridLines(gridLines, rgb, cols, screenPoint)); + } + } + } + } +} + // Method Description: // - Updates the window's title string. For GDI, this does nothing, because the // title must be updated on the main window's windowproc thread. -// Arguments: -// - newTitle: the new string to use for the title of the window // Return Value: // - S_OK if PostMessageW succeeded, otherwise E_FAIL -[[nodiscard]] HRESULT GdiEngine::_DoUpdateTitle(_In_ const std::wstring_view /*newTitle*/) noexcept +[[nodiscard]] HRESULT GdiEngine::_UpdateTitle(const std::wstring_view newTitle) noexcept { - // the CM_UPDATE_TITLE handler in windowproc will query the updated title. - return PostMessageW(_hwndTargetWindow, CM_UPDATE_TITLE, 0, (LPARAM) nullptr) ? S_OK : E_FAIL; + HRESULT hr = S_FALSE; + if (newTitle != _lastFrameTitle) + { + // the CM_UPDATE_TITLE handler in windowproc will query the updated title. + hr = PostMessageW(_hwndTargetWindow, CM_UPDATE_TITLE, 0, (LPARAM) nullptr) ? S_OK : E_FAIL; + _lastFrameTitle = newTitle; + _titleChanged = false; + hr = S_OK; + } + return hr; } // Routine Description: diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 3d6631e410e..0c95a93118a 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -19,6 +19,7 @@ Author(s): #include "FontInfoDesired.hpp" #include "IRenderData.hpp" #include "../../buffer/out/LineRendition.hpp" +#include "../../types/inc/viewport.hpp" namespace Microsoft::Console::Render { @@ -54,6 +55,8 @@ namespace Microsoft::Console::Render public: [[nodiscard]] virtual HRESULT StartPaint() noexcept = 0; + [[nodiscard]] virtual HRESULT PaintFrame(IRenderData* pData) noexcept = 0; + [[nodiscard]] virtual HRESULT EndPaint() noexcept = 0; [[nodiscard]] virtual bool RequiresContinuousRedraw() noexcept = 0; @@ -62,45 +65,20 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept = 0; - [[nodiscard]] virtual HRESULT ScrollFrame() noexcept = 0; - - [[nodiscard]] virtual HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept = 0; + [[nodiscard]] virtual bool TriggerRedraw(IRenderData* pData, const Microsoft::Console::Types::Viewport& region) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept = 0; - [[nodiscard]] virtual HRESULT InvalidateSelection(const std::vector& rectangles) noexcept = 0; - [[nodiscard]] virtual HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept = 0; + [[nodiscard]] virtual HRESULT TriggerSelection(IRenderData* pData) noexcept = 0; + [[nodiscard]] virtual HRESULT TriggerScroll(const COORD* const pcoordDelta) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateAll() noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept = 0; [[nodiscard]] virtual HRESULT InvalidateTitle(const std::wstring_view proposedTitle) noexcept = 0; - [[nodiscard]] virtual HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept = 0; - - [[nodiscard]] virtual HRESULT ResetLineTransform() noexcept = 0; - [[nodiscard]] virtual HRESULT PrepareLineTransform(const LineRendition lineRendition, - const size_t targetRow, - const size_t viewportLeft) noexcept = 0; - - [[nodiscard]] virtual HRESULT PaintBackground() noexcept = 0; - [[nodiscard]] virtual HRESULT PaintBufferLine(gsl::span const clusters, - const COORD coord, - const bool fTrimLeft, - const bool lineWrapped) noexcept = 0; - [[nodiscard]] virtual HRESULT PaintBufferGridLines(const GridLines lines, - const COLORREF color, - const size_t cchLine, - const COORD coordTarget) noexcept = 0; - [[nodiscard]] virtual HRESULT PaintSelection(const SMALL_RECT rect) noexcept = 0; - - [[nodiscard]] virtual HRESULT PaintCursor(const CursorOptions& options) noexcept = 0; - - [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, - const gsl::not_null pData, - const bool isSettingDefaultBrushes) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateDpi(const int iDpi) noexcept = 0; - [[nodiscard]] virtual HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept = 0; + virtual COORD UpdateViewport(IRenderData* pData) noexcept = 0; [[nodiscard]] virtual HRESULT GetProposedFont(const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo, @@ -109,7 +87,6 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT GetDirtyArea(gsl::span& area) noexcept = 0; [[nodiscard]] virtual HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept = 0; [[nodiscard]] virtual HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept = 0; - [[nodiscard]] virtual HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept = 0; }; inline Microsoft::Console::Render::IRenderEngine::~IRenderEngine() {} diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 725347d8215..869bc108e4b 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -21,6 +21,27 @@ Author(s): #pragma once namespace Microsoft::Console::Render { + struct BufferLineRenderData + { + IRenderData* pData; + const TextBuffer& buffer; + // Text position in buffer + COORD bufferPosition; + // Text bounds limit in buffer. + Microsoft::Console::Types::Viewport bufferLimit; + // Target screen position for drawing. + COORD screenPosition; + // Used by GDI Engine. + Microsoft::Console::Types::Viewport visible; + // Used by GDI Engine. + LineRendition lineRendition; + // Used by GDI Engine. + bool needLineTransformation; + bool lineWrapped; + bool globalInvert; + bool gridLineDrawingAllowed; + }; + class RenderEngineBase : public IRenderEngine { public: @@ -28,32 +49,63 @@ namespace Microsoft::Console::Render protected: RenderEngineBase(); + RenderEngineBase(const Microsoft::Console::Types::Viewport initialViewport); RenderEngineBase(const RenderEngineBase&) = default; RenderEngineBase(RenderEngineBase&&) = default; RenderEngineBase& operator=(const RenderEngineBase&) = default; RenderEngineBase& operator=(RenderEngineBase&&) = default; - public: - [[nodiscard]] HRESULT InvalidateTitle(const std::wstring_view proposedTitle) noexcept override; + [[nodiscard]] std::optional _GetCursorInfo(IRenderData* pData); + void _LoopDirtyLines(IRenderData* pData, std::function action); + void _LoopOverlay(IRenderData* pData, std::function action); + void _LoopSelection(IRenderData* pData, std::function action); + + GridLines _CalculateGridLines(IRenderData* pData, + const TextAttribute textAttribute, + const COORD coordTarget); - [[nodiscard]] HRESULT UpdateTitle(const std::wstring_view newTitle) noexcept override; + static GridLines s_GetGridlines(const TextAttribute& textAttribute) noexcept; - [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; + std::vector _GetSelectionRects(IRenderData* pData) noexcept; + std::vector _CalculateCurrentSelection(IRenderData* pData) noexcept; - [[nodiscard]] HRESULT ResetLineTransform() noexcept override; - [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, - const size_t targetRow, - const size_t viewportLeft) noexcept override; + void _ScrollPreviousSelection(const til::point delta); + + static constexpr bool s_IsAllSpaces(const std::wstring_view v) noexcept + { + // first non-space char is not found (is npos) + return v.find_first_not_of(L" ") == decltype(v)::npos; + } + + public: + [[nodiscard]] HRESULT InvalidateTitle(const std::wstring_view proposedTitle) noexcept override; + COORD UpdateViewport(IRenderData* pData) noexcept override; + [[nodiscard]] bool TriggerRedraw(IRenderData* pData, const Microsoft::Console::Types::Viewport& region) noexcept override; + [[nodiscard]] HRESULT TriggerSelection(IRenderData* /*pData*/) noexcept override { return S_OK; }; + [[nodiscard]] HRESULT TriggerScroll(const COORD* const /*pcoordDelta*/) noexcept override { return S_OK; }; [[nodiscard]] virtual bool RequiresContinuousRedraw() noexcept override; + [[nodiscard]] virtual HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept = 0; + [[nodiscard]] virtual HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept = 0; void WaitUntilCanRender() noexcept override; - protected: - [[nodiscard]] virtual HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept = 0; + void UpdateLastHoveredInterval(const std::optional::interval>& newInterval) noexcept + { + _hoveredInterval = newInterval; + } + protected: bool _titleChanged; std::wstring _lastFrameTitle; + + std::vector _clusterBuffer; + std::optional::interval> _hoveredInterval; + + Microsoft::Console::Types::Viewport _viewport; + std::vector _previousSelection; + + static constexpr float _shrinkThreshold = 0.8f; }; inline Microsoft::Console::Render::RenderEngineBase::~RenderEngineBase() {} diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index ba7ca8e5f64..458b562de90 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -20,7 +20,6 @@ UiaEngine::UiaEngine(IUiaEventDispatcher* dispatcher) : _textBufferChanged{ false }, _cursorChanged{ false }, _isEnabled{ true }, - _prevSelection{}, _prevCursorRegion{}, RenderEngineBase() { @@ -104,15 +103,17 @@ CATCH_RETURN(); // - rectangles - One or more rectangles describing character positions on the grid // Return Value: // - S_OK -[[nodiscard]] HRESULT UiaEngine::InvalidateSelection(const std::vector& rectangles) noexcept +[[nodiscard]] HRESULT UiaEngine::TriggerSelection(IRenderData* pData) noexcept { + const auto rectangles = _CalculateCurrentSelection(pData); + // early exit: different number of rows - if (_prevSelection.size() != rectangles.size()) + if (_previousSelection.size() != rectangles.size()) { try { _selectionChanged = true; - _prevSelection = rectangles; + _previousSelection = rectangles; } CATCH_LOG_RETURN_HR(E_FAIL); return S_OK; @@ -122,14 +123,14 @@ CATCH_RETURN(); { try { - const auto prevRect = _prevSelection.at(i); + const auto prevRect = _previousSelection.at(i); const auto newRect = rectangles.at(i); // if any value is different, selection has changed if (prevRect.Top != newRect.Top || prevRect.Right != newRect.Right || prevRect.Left != newRect.Left || prevRect.Bottom != newRect.Bottom) { _selectionChanged = true; - _prevSelection = rectangles; + _previousSelection = rectangles; return S_OK; } } @@ -141,19 +142,6 @@ CATCH_RETURN(); return S_OK; } -// Routine Description: -// - Scrolls the existing dirty region (if it exists) and -// invalidates the area that is uncovered in the window. -// Arguments: -// - pcoordDelta - The number of characters to move and uncover. -// - -Y is up, Y is down, -X is left, X is right. -// Return Value: -// - S_OK -[[nodiscard]] HRESULT UiaEngine::InvalidateScroll(const COORD* const /*pcoordDelta*/) noexcept -{ - return S_FALSE; -} - // Routine Description: // - Notifies to repaint everything. // - NOTE: Use sparingly. Only use when something that could affect the entire @@ -216,6 +204,11 @@ CATCH_RETURN(); return S_OK; } +[[nodiscard]] HRESULT UiaEngine::PaintFrame(IRenderData* /*pData*/) noexcept +{ + return S_OK; +} + // Routine Description: // - Ends batch drawing and notifies automation clients of updated regions // Arguments: @@ -274,106 +267,6 @@ CATCH_RETURN(); return S_FALSE; } -// Routine Description: -// - This is currently unused. -// Arguments: -// - -// Return Value: -// - S_FALSE -[[nodiscard]] HRESULT UiaEngine::ScrollFrame() noexcept -{ - return S_FALSE; -} - -// Routine Description: -// - Paints the background of the invalid area of the frame. -// For UIA, this doesn't mean anything. So do nothing. -// Arguments: -// - -// Return Value: -// - S_FALSE since we do nothing -[[nodiscard]] HRESULT UiaEngine::PaintBackground() noexcept -{ - return S_FALSE; -} - -// Routine Description: -// - Places one line of text onto the screen at the given position -// For UIA, this doesn't mean anything. So do nothing. -// Arguments: -// - clusters - Iterable collection of cluster information (text and columns it should consume) -// - coord - Character coordinate position in the cell grid -// - fTrimLeft - Whether or not to trim off the left half of a double wide character -// Return Value: -// - S_FALSE -[[nodiscard]] HRESULT UiaEngine::PaintBufferLine(gsl::span const /*clusters*/, - COORD const /*coord*/, - const bool /*trimLeft*/, - const bool /*lineWrapped*/) noexcept -{ - return S_FALSE; -} - -// Routine Description: -// - Paints lines around cells (draws in pieces of the grid) -// For UIA, this doesn't mean anything. So do nothing. -// Arguments: -// - lines - -// - color - -// - cchLine - -// - coordTarget - -// Return Value: -// - S_FALSE -[[nodiscard]] HRESULT UiaEngine::PaintBufferGridLines(GridLines const /*lines*/, - COLORREF const /*color*/, - size_t const /*cchLine*/, - COORD const /*coordTarget*/) noexcept -{ - return S_FALSE; -} - -// Routine Description: -// - Reads the selected area, selection mode, and active screen buffer -// from the global properties and dispatches a GDI invert on the selected text area. -// Because the selection is the responsibility of the terminal, and not the -// host, render nothing. -// Arguments: -// - rect - Rectangle to invert or highlight to make the selection area -// Return Value: -// - S_FALSE -[[nodiscard]] HRESULT UiaEngine::PaintSelection(const SMALL_RECT /*rect*/) noexcept -{ - return S_FALSE; -} - -// Routine Description: -// - Draws the cursor on the screen -// For UIA, this doesn't mean anything. So do nothing. -// Arguments: -// - options - Packed options relevant to how to draw the cursor -// Return Value: -// - S_FALSE -[[nodiscard]] HRESULT UiaEngine::PaintCursor(const CursorOptions& /*options*/) noexcept -{ - return S_FALSE; -} - -// Routine Description: -// - Updates the default brush colors used for drawing -// For UIA, this doesn't mean anything. So do nothing. -// Arguments: -// - textAttributes - -// - pData - -// - isSettingDefaultBrushes - -// Return Value: -// - S_FALSE since we do nothing -[[nodiscard]] HRESULT UiaEngine::UpdateDrawingBrushes(const TextAttribute& /*textAttributes*/, - const gsl::not_null /*pData*/, - const bool /*isSettingDefaultBrushes*/) noexcept -{ - return S_FALSE; -} - // Routine Description: // - Updates the font used for drawing // Arguments: @@ -398,17 +291,6 @@ CATCH_RETURN(); return S_FALSE; } -// Method Description: -// - This method will update our internal reference for how big the viewport is. -// Arguments: -// - srNewViewport - The bounds of the new viewport. -// Return Value: -// - HRESULT S_OK -[[nodiscard]] HRESULT UiaEngine::UpdateViewport(const SMALL_RECT /*srNewViewport*/) noexcept -{ - return S_FALSE; -} - // Routine Description: // - Currently unused by this renderer // Arguments: @@ -462,15 +344,3 @@ CATCH_RETURN(); { return S_FALSE; } - -// Method Description: -// - Updates the window's title string. -// - Currently unused by this renderer. -// Arguments: -// - newTitle: the new string to use for the title of the window -// Return Value: -// - S_FALSE -[[nodiscard]] HRESULT UiaEngine::_DoUpdateTitle(_In_ const std::wstring_view /*newTitle*/) noexcept -{ - return S_FALSE; -} diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 3ead4673b70..30561ef7f0f 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -35,37 +35,21 @@ namespace Microsoft::Console::Render // IRenderEngine Members [[nodiscard]] HRESULT StartPaint() noexcept override; + [[nodiscard]] HRESULT PaintFrame(IRenderData* pData) noexcept override; [[nodiscard]] HRESULT EndPaint() noexcept override; [[nodiscard]] HRESULT Present() noexcept override; [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; - [[nodiscard]] HRESULT ScrollFrame() noexcept override; - [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; [[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override; [[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; - [[nodiscard]] HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; - [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + [[nodiscard]] HRESULT TriggerSelection(IRenderData* pData) noexcept override; [[nodiscard]] HRESULT InvalidateAll() noexcept override; [[nodiscard]] HRESULT InvalidateCircling(_Out_ bool* const pForcePaint) noexcept override; - [[nodiscard]] HRESULT PaintBackground() noexcept override; - [[nodiscard]] HRESULT PaintBufferLine(gsl::span const clusters, - COORD const coord, - bool const fTrimLeft, - const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; - [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; - - [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; - - [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, - const gsl::not_null pData, - const bool isSettingDefaultBrushes) noexcept override; [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(int const iDpi) noexcept override; - [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; @@ -73,9 +57,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; - protected: - [[nodiscard]] HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override; - private: bool _isEnabled; bool _isPainting; @@ -85,7 +66,6 @@ namespace Microsoft::Console::Render Microsoft::Console::Types::IUiaEventDispatcher* _dispatcher; - std::vector _prevSelection; SMALL_RECT _prevCursorRegion; }; } diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index a948acd827e..e10921a045e 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -380,7 +380,7 @@ try // statement here. // Move the cursor to the bottom of the current viewport - const short bottom = _lastViewport.BottomInclusive(); + const short bottom = _viewport.BottomInclusive(); RETURN_IF_FAILED(_MoveCursor({ 0, bottom })); // Emit some number of newlines to create space in the buffer. RETURN_IF_FAILED(_Write(std::string(absDy, '\n'))); @@ -429,7 +429,7 @@ try // changes _above_ the wrapped line, that we maintain the wrap state in // the Terminal. const til::rectangle lastCellOfWrappedRow{ - til::point{ _lastViewport.RightInclusive(), _wrappedRow.value() }, + til::point{ _viewport.RightInclusive(), _wrappedRow.value() }, til::size{ 1, 1 } }; _trace.TraceInvalidate(lastCellOfWrappedRow); @@ -465,7 +465,7 @@ CATCH_RETURN(); // console would like us to move while scrolling. // Return Value: // - S_OK if we succeeded, else an appropriate HRESULT for safemath failure -[[nodiscard]] HRESULT XtermEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept +[[nodiscard]] HRESULT XtermEngine::TriggerScroll(const COORD* const pcoordDelta) noexcept try { const til::point delta{ *pcoordDelta }; @@ -480,6 +480,8 @@ try _scrollDelta += delta; } + _ScrollPreviousSelection(*pcoordDelta); + return S_OK; } CATCH_RETURN(); @@ -541,19 +543,28 @@ CATCH_RETURN(); // - newTitle: the new string to use for the title of the window // Return Value: // - S_OK -[[nodiscard]] HRESULT XtermEngine::_DoUpdateTitle(const std::wstring_view newTitle) noexcept +[[nodiscard]] HRESULT XtermEngine::_UpdateTitle(const std::wstring_view newTitle) noexcept { - // inbox telnet uses xterm-ascii as its mode. If we're in ascii mode, don't - // do anything, to maintain compatibility. - if (_fUseAsciiOnly) + HRESULT hr = S_FALSE; + if (newTitle != _lastFrameTitle) { - return S_OK; - } + // inbox telnet uses xterm-ascii as its mode. If we're in ascii mode, don't + // do anything, to maintain compatibility. + if (_fUseAsciiOnly) + { + hr = S_OK; + } - try - { - const auto converted = ConvertToA(CP_UTF8, newTitle); - return VtEngine::_ChangeTitle(converted); + try + { + const auto converted = ConvertToA(CP_UTF8, newTitle); + hr = VtEngine::_ChangeTitle(converted); + } + CATCH_RETURN(); + + _lastFrameTitle = newTitle; + _titleChanged = false; + hr = S_OK; } - CATCH_RETURN(); + return hr; } diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp index 1dcf2d17349..e211e1cd51c 100644 --- a/src/renderer/vt/XtermEngine.hpp +++ b/src/renderer/vt/XtermEngine.hpp @@ -47,7 +47,7 @@ namespace Microsoft::Console::Render const bool lineWrapped) noexcept override; [[nodiscard]] HRESULT ScrollFrame() noexcept override; - [[nodiscard]] HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept override; + [[nodiscard]] HRESULT TriggerScroll(const COORD* const pcoordDelta) noexcept override; [[nodiscard]] HRESULT WriteTerminalW(const std::wstring_view str) noexcept override; @@ -59,7 +59,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _MoveCursor(const COORD coord) noexcept override; - [[nodiscard]] HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override; + [[nodiscard]] HRESULT _UpdateTitle(const std::wstring_view newTitle) noexcept; #ifdef UNIT_TESTING friend class VtRendererTest; diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp index 1856ac4cdd4..7c05ecc9634 100644 --- a/src/renderer/vt/invalidate.cpp +++ b/src/renderer/vt/invalidate.cpp @@ -24,21 +24,6 @@ using namespace Microsoft::Console::Render; return S_OK; } -// Routine Description: -// - Notifies us that the console has changed the selection region and would -// like it updated -// Arguments: -// - rectangles - Vector of rectangles to draw, line by line -// Return Value: -// - S_OK -[[nodiscard]] HRESULT VtEngine::InvalidateSelection(const std::vector& /*rectangles*/) noexcept -{ - // Selection shouldn't be handled bt the VT Renderer Host, it should be - // handled by the client. - - return S_OK; -} - // Routine Description: // - Notifies us that the console has changed the character region specified. // - NOTE: This typically triggers on cursor or text buffer changes @@ -91,7 +76,7 @@ CATCH_RETURN(); [[nodiscard]] HRESULT VtEngine::InvalidateAll() noexcept try { - _trace.TraceInvalidateAll(_lastViewport.ToOrigin().ToInclusive()); + _trace.TraceInvalidateAll(_viewport.ToOrigin().ToInclusive()); _invalidMap.set_all(); return S_OK; } diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index f8b8a126654..67d6053462e 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -6,6 +6,7 @@ #include "vtrenderer.hpp" #include "../../inc/conattrs.hpp" #include "../../types/inc/convert.hpp" +#include "../../buffer/out/textBuffer.hpp" #pragma hdrstop using namespace Microsoft::Console::Render; @@ -34,7 +35,7 @@ using namespace Microsoft::Console::Types; _quickReturn = !somethingToDo; _trace.TraceStartPaint(_quickReturn, _invalidMap, - _lastViewport.ToInclusive(), + _viewport.ToInclusive(), _scrollDelta, _cursorMoved, _wrappedRow); @@ -42,6 +43,27 @@ using namespace Microsoft::Console::Types; return _quickReturn ? S_FALSE : S_OK; } +[[nodiscard]] HRESULT VtEngine::PaintFrame(IRenderData* pData) noexcept +{ + // Prep Colors + RETURN_IF_FAILED(UpdateDrawingBrushes(pData->GetDefaultBrushColors(), pData, true)); + + // Perform Scroll Operations + RETURN_IF_FAILED(ScrollFrame()); + + // Paint Rows of Text + _LoopDirtyLines(pData, std::bind(&VtEngine::_PaintBufferLineHelper, this, std::placeholders::_1)); + + // Paint Cursor + const auto cursorInfo = _GetCursorInfo(pData); + if (cursorInfo.has_value()) + { + LOG_IF_FAILED(PaintCursor(cursorInfo.value())); + } + + return S_OK; +} + // Routine Description: // - EndPaint helper to perform the final cleanup after painting. If we // returned S_FALSE from StartPaint, there's no guarantee this was called. @@ -100,17 +122,6 @@ using namespace Microsoft::Console::Types; return S_FALSE; } -// Routine Description: -// - Paints the background of the invalid area of the frame. -// Arguments: -// - -// Return Value: -// - S_OK -[[nodiscard]] HRESULT VtEngine::PaintBackground() noexcept -{ - return S_OK; -} - // Routine Description: // - Draws one line of the buffer to the screen. Writes the characters to the // pipe. If the characters are outside the ASCII range (0-0x7f), then @@ -133,23 +144,6 @@ using namespace Microsoft::Console::Types; return VtEngine::_PaintAsciiBufferLine(clusters, coord); } -// Method Description: -// - Draws up to one line worth of grid lines on top of characters. -// Arguments: -// - lines - Enum defining which edges of the rectangle to draw -// - color - The color to use for drawing the edges. -// - cchLine - How many characters we should draw the grid lines along (left to right in a row) -// - coordTarget - The starting X/Y position of the first character to draw on. -// Return Value: -// - S_OK -[[nodiscard]] HRESULT VtEngine::PaintBufferGridLines(const GridLines /*lines*/, - const COLORREF /*color*/, - const size_t /*cchLine*/, - const COORD /*coordTarget*/) noexcept -{ - return S_OK; -} - // Routine Description: // - Draws the cursor on the screen // Arguments: @@ -166,21 +160,112 @@ using namespace Microsoft::Console::Types; return S_OK; } -// Routine Description: -// - Inverts the selected region on the current screen buffer. -// - Reads the selected area, selection mode, and active screen buffer -// from the global properties and dispatches a GDI invert on the selected text area. -// Because the selection is the responsibility of the terminal, and not the -// host, render nothing. -// Arguments: -// - rect - Rectangle to invert or highlight to make the selection area -// Return Value: -// - S_OK -[[nodiscard]] HRESULT VtEngine::PaintSelection(const SMALL_RECT /*rect*/) noexcept +void VtEngine::_PaintBufferLineHelper(const BufferLineRenderData& renderData) { - return S_OK; -} + // Retrieve the cell information iterator limited to just this line we want to redraw. + auto it = renderData.buffer.GetCellDataAt(renderData.bufferPosition, renderData.bufferLimit); + if (!it) + { + return; + } + + auto pData = renderData.pData; + size_t cols = 0; + + // Retrieve the first color. + auto color = it->TextAttr(); + // And hold the point where we should start drawing. + auto screenPoint = renderData.screenPosition; + + // 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(currentRunColor, pData, false)); + + // Advance the point by however many columns we've just outputted and reset the accumulator. + screenPoint.X += gsl::narrow(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. + bool trimLeft = false; + + // Run contains wide character (>1 columns) + bool 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 + { + COORD thisPoint{ screenPoint.X + gsl::narrow(cols), screenPoint.Y }; + if (color != it->TextAttr()) + { + 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 (!s_IsAllSpaces(it->Chars()) || !newAttr.HasIdenticalVisualRepresentationForBlankSpace(color, renderData.globalInvert)) + { + color = newAttr; + 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. + size_t columnCount = 0; + + // 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().IsTrailing()) + { + // 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 = it->Columns() + 1; + _clusterBuffer.emplace_back(it->Chars(), columnCount); + } + // Otherwise if it's not a special case, just insert it as is. + else + { + columnCount = it->Columns(); + _clusterBuffer.emplace_back(it->Chars(), columnCount); + } + + if (columnCount > 1) + { + containsWideCharacter = true; + } + + // Advance the cluster and column counts. + it += std::max(it->Columns(), 1); // prevent infinite loop for no visible columns + cols += columnCount; + + } while (it); + + // Do the painting. + THROW_IF_FAILED(PaintBufferLine({ _clusterBuffer.data(), _clusterBuffer.size() }, screenPoint, trimLeft, renderData.lineWrapped)); + } +} // Routine Description: // - Write a VT sequence to change the current colors of text. Writes true RGB // color sequences. @@ -436,7 +521,7 @@ using namespace Microsoft::Console::Types; const bool useEraseChar = (optimalToUseECH) && (!_newBottomLine) && (!_clearedAllThisFrame); - const bool printingBottomLine = coord.Y == _lastViewport.BottomInclusive(); + const bool printingBottomLine = coord.Y == _viewport.BottomInclusive(); // GH#5502 - If the background color of the "new bottom line" is different // than when we emitted the line, we can't optimize out the spaces from it. @@ -519,7 +604,7 @@ using namespace Microsoft::Console::Types; // GH#1245: This needs to be RightExclusive, _not_ inclusive. Otherwise, we // won't update our internal cursor position tracker correctly at the last // character of the row. - if (_lastText.X < _lastViewport.RightExclusive()) + if (_lastText.X < _viewport.RightExclusive()) { _lastText.X += static_cast(columnsActual); } @@ -529,7 +614,7 @@ using namespace Microsoft::Console::Types; // Mark that we're in the delayed EOL wrap state - we don't want to be // clever about how we move the cursor in this state, since different // terminals will handle a backspace differently in this state. - if (_lastText.X >= _lastViewport.RightInclusive()) + if (_lastText.X >= _viewport.RightInclusive()) { _delayedEolWrap = true; } @@ -551,7 +636,7 @@ using namespace Microsoft::Console::Types; // before we need to print new text. _deferredCursorPos = { _lastText.X + sNumSpaces, _lastText.Y }; - if (_deferredCursorPos.X <= _lastViewport.RightInclusive()) + if (_deferredCursorPos.X <= _viewport.RightInclusive()) { RETURN_IF_FAILED(_EraseCharacter(sNumSpaces)); } @@ -588,17 +673,3 @@ using namespace Microsoft::Console::Types; return S_OK; } - -// Method Description: -// - Updates the window's title string. Emits the VT sequence to SetWindowTitle. -// Because wintelnet does not understand these sequences by default, we -// don't do anything by default. Other modes can implement if they support -// the sequence. -// Arguments: -// - newTitle: the new string to use for the title of the window -// Return Value: -// - S_OK -[[nodiscard]] HRESULT VtEngine::_DoUpdateTitle(const std::wstring_view /*newTitle*/) noexcept -{ - return S_OK; -} diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 93ad4ade64a..cd8dbdaeda8 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -27,10 +27,9 @@ const COORD VtEngine::INVALID_COORDS = { -1, -1 }; // - An instance of a Renderer. VtEngine::VtEngine(_In_ wil::unique_hfile pipe, const Viewport initialViewport) : - RenderEngineBase(), + RenderEngineBase(initialViewport), _hFile(std::move(pipe)), _lastTextAttributes(INVALID_COLOR, INVALID_COLOR), - _lastViewport(initialViewport), _pool(til::pmr::get_default_resource()), _invalidMap(initialViewport.Dimensions(), false, &_pool), _lastText({ 0 }), @@ -211,13 +210,23 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, // - srNewViewport - The bounds of the new viewport. // Return Value: // - HRESULT S_OK -[[nodiscard]] HRESULT VtEngine::UpdateViewport(const SMALL_RECT srNewViewport) noexcept +COORD VtEngine::UpdateViewport(IRenderData* pData) noexcept +{ + SMALL_RECT const srNewViewport = pData->GetViewport().ToInclusive(); + return UpdateViewport(srNewViewport); +} + +COORD VtEngine::UpdateViewport(const SMALL_RECT srNewViewport) noexcept { HRESULT hr = S_OK; - const Viewport oldView = _lastViewport; + const Viewport oldView = _viewport; const Viewport newView = Viewport::FromInclusive(srNewViewport); - _lastViewport = newView; + COORD coordDelta; + coordDelta.X = oldView.Left() - newView.Left(); + coordDelta.Y = oldView.Top() - newView.Top(); + + _viewport = newView; if ((oldView.Height() != newView.Height()) || (oldView.Width() != newView.Width())) { @@ -256,14 +265,13 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, // Viewport is smaller now - just update it all. if (oldView.Height() > newView.Height() || oldView.Width() > newView.Width()) { - hr = InvalidateAll(); + LOG_IF_FAILED(InvalidateAll()); } } } - return hr; + return coordDelta; } - // Method Description: // - This method will figure out what the new font should be given the starting font information and a DPI. // - When the final font is determined, the FontInfo structure given will be updated with the actual resulting font chosen as the nearest match. diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index f7b0a788b66..d5cec87a517 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -45,8 +45,6 @@ namespace Microsoft::Console::Render virtual ~VtEngine() override = default; - [[nodiscard]] HRESULT InvalidateSelection(const std::vector& rectangles) noexcept override; - [[nodiscard]] virtual HRESULT InvalidateScroll(const COORD* const pcoordDelta) noexcept = 0; [[nodiscard]] HRESULT InvalidateSystem(const RECT* const prcDirtyClient) noexcept override; [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; [[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept override; @@ -55,23 +53,19 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT PrepareForTeardown(_Out_ bool* const pForcePaint) noexcept override; [[nodiscard]] virtual HRESULT StartPaint() noexcept override; + [[nodiscard]] virtual HRESULT PaintFrame(IRenderData* pData) noexcept override; + [[nodiscard]] virtual HRESULT EndPaint() noexcept override; [[nodiscard]] virtual HRESULT Present() noexcept override; [[nodiscard]] virtual HRESULT ScrollFrame() noexcept = 0; - [[nodiscard]] HRESULT PaintBackground() noexcept override; [[nodiscard]] virtual HRESULT PaintBufferLine(gsl::span const clusters, const COORD coord, const bool trimLeft, - const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(const GridLines lines, - const COLORREF color, - const size_t cchLine, - const COORD coordTarget) noexcept override; - [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; + const bool lineWrapped) noexcept; - [[nodiscard]] virtual HRESULT PaintCursor(const CursorOptions& options) noexcept override; + [[nodiscard]] virtual HRESULT PaintCursor(const CursorOptions& options) noexcept; [[nodiscard]] virtual HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, @@ -79,7 +73,8 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT UpdateFont(const FontInfoDesired& pfiFontInfoDesired, _Out_ FontInfo& pfiFontInfo) noexcept override; [[nodiscard]] HRESULT UpdateDpi(const int iDpi) noexcept override; - [[nodiscard]] HRESULT UpdateViewport(const SMALL_RECT srNewViewport) noexcept override; + COORD UpdateViewport(IRenderData* pData) noexcept override; + COORD UpdateViewport(const SMALL_RECT srNewViewport) noexcept; [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& FontDesired, _Out_ FontInfo& Font, @@ -117,8 +112,6 @@ namespace Microsoft::Console::Render TextAttribute _lastTextAttributes; - Microsoft::Console::Types::Viewport _lastViewport; - std::pmr::unsynchronized_pool_resource _pool; til::pmr::bitmap _invalidMap; @@ -153,6 +146,8 @@ namespace Microsoft::Console::Render bool _resizeQuirk{ false }; std::optional _newBottomLineBG{ std::nullopt }; + void _PaintBufferLineHelper(const BufferLineRenderData& renderData); + [[nodiscard]] HRESULT _Write(std::string_view const str) noexcept; [[nodiscard]] HRESULT _Flush() noexcept; @@ -233,8 +228,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _WriteTerminalUtf8(const std::wstring_view str) noexcept; [[nodiscard]] HRESULT _WriteTerminalAscii(const std::wstring_view str) noexcept; - [[nodiscard]] virtual HRESULT _DoUpdateTitle(const std::wstring_view newTitle) noexcept override; - /////////////////////////// Unit Testing Helpers /////////////////////////// #ifdef UNIT_TESTING std::function _pfnTestCallback; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index ed2c027ee4a..ae2d97c8ec1 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -228,11 +228,6 @@ bool WddmConEngine::IsInitialized() return S_FALSE; } -[[nodiscard]] HRESULT WddmConEngine::ScrollFrame() noexcept -{ - return S_OK; -} - [[nodiscard]] HRESULT WddmConEngine::PaintBackground() noexcept { RETURN_IF_HANDLE_INVALID(_hWddmConCtx); @@ -287,24 +282,6 @@ bool WddmConEngine::IsInitialized() CATCH_RETURN(); } -[[nodiscard]] HRESULT WddmConEngine::PaintBufferGridLines(GridLines const /*lines*/, - COLORREF const /*color*/, - size_t const /*cchLine*/, - COORD const /*coordTarget*/) noexcept -{ - return S_OK; -} - -[[nodiscard]] HRESULT WddmConEngine::PaintSelection(const SMALL_RECT /*rect*/) noexcept -{ - return S_OK; -} - -[[nodiscard]] HRESULT WddmConEngine::PaintCursor(const CursorOptions& /*options*/) noexcept -{ - return S_OK; -} - [[nodiscard]] HRESULT WddmConEngine::UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null /*pData*/, bool const /*isSettingDefaultBrushes*/) noexcept @@ -406,15 +383,3 @@ RECT WddmConEngine::GetDisplaySize() *pResult = false; return S_OK; } - -// Method Description: -// - Updates the window's title string. -// Does nothing for WddmCon. -// Arguments: -// - newTitle: the new string to use for the title of the window -// Return Value: -// - S_OK -[[nodiscard]] HRESULT WddmConEngine::_DoUpdateTitle(_In_ const std::wstring_view /*newTitle*/) noexcept -{ - return S_OK; -} diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index 6898f1ecef8..ec24f63a586 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -45,10 +45,6 @@ namespace Microsoft::Console::Render const COORD coord, const bool trimLeft, const bool lineWrapped) noexcept override; - [[nodiscard]] HRESULT PaintBufferGridLines(GridLines const lines, COLORREF const color, size_t const cchLine, COORD const coordTarget) noexcept override; - [[nodiscard]] HRESULT PaintSelection(const SMALL_RECT rect) noexcept override; - - [[nodiscard]] HRESULT PaintCursor(const CursorOptions& options) noexcept override; [[nodiscard]] HRESULT UpdateDrawingBrushes(const TextAttribute& textAttributes, const gsl::not_null pData, @@ -63,9 +59,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; - protected: - [[nodiscard]] HRESULT _DoUpdateTitle(_In_ const std::wstring_view newTitle) noexcept override; - private: HANDLE _hWddmConCtx;