diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 8b0943584..96c6fe7f4 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -521,6 +521,8 @@ DECAUPSS DECAWM DECCKM DECCOLM +DECDHL +DECDWL DECEKBD DECID DECKPAM @@ -557,6 +559,7 @@ DECSR decstandar DECSTBM DECSTR +DECSWL DECTCEM Dedupe deduplicated @@ -2825,6 +2828,7 @@ Xes XES xff XFile +XFORM XManifest XMath XMFLOAT diff --git a/src/buffer/out/LineRendition.hpp b/src/buffer/out/LineRendition.hpp new file mode 100644 index 000000000..8bf880e6a --- /dev/null +++ b/src/buffer/out/LineRendition.hpp @@ -0,0 +1,36 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- LineRendition.hpp + +Abstract: +- Enumerated type for the VT line rendition attribute. This determines the + width and height scaling with which each line is rendered. + +--*/ + +#pragma once + +enum class LineRendition +{ + SingleWidth, + DoubleWidth, + DoubleHeightTop, + DoubleHeightBottom +}; + +constexpr SMALL_RECT ScreenToBufferLine(const SMALL_RECT& line, const LineRendition lineRendition) +{ + // Use shift right to quickly divide the Left and Right by 2 for double width lines. + const auto scale = lineRendition == LineRendition::SingleWidth ? 0 : 1; + return { line.Left >> scale, line.Top, line.Right >> scale, line.Bottom }; +} + +constexpr SMALL_RECT BufferToScreenLine(const SMALL_RECT& line, const LineRendition lineRendition) +{ + // Use shift left to quickly multiply the Left and Right by 2 for double width lines. + const SHORT scale = lineRendition == LineRendition::SingleWidth ? 0 : 1; + return { line.Left << scale, line.Top, (line.Right << scale) + scale, line.Bottom }; +} diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index de88da978..4b4a44bf1 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -21,6 +21,7 @@ ROW::ROW(const SHORT rowId, const unsigned short rowWidth, const TextAttribute f _rowWidth{ rowWidth }, _charRow{ rowWidth, this }, _attrRow{ rowWidth, fillAttribute }, + _lineRendition{ LineRendition::SingleWidth }, _wrapForced{ false }, _doubleBytePadded{ false }, _pParent{ pParent } @@ -35,6 +36,7 @@ ROW::ROW(const SHORT rowId, const unsigned short rowWidth, const TextAttribute f // - bool ROW::Reset(const TextAttribute Attr) { + _lineRendition = LineRendition::SingleWidth; _wrapForced = false; _doubleBytePadded = false; _charRow.Reset(); diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index 441f9bd94..ae4d0d54d 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -21,6 +21,7 @@ Revision History: #pragma once #include "AttrRow.hpp" +#include "LineRendition.hpp" #include "OutputCell.hpp" #include "OutputCellIterator.hpp" #include "CharRow.hpp" @@ -48,6 +49,9 @@ public: const ATTR_ROW& GetAttrRow() const noexcept { return _attrRow; } ATTR_ROW& GetAttrRow() noexcept { return _attrRow; } + LineRendition GetLineRendition() const noexcept { return _lineRendition; } + void SetLineRendition(const LineRendition lineRendition) noexcept { _lineRendition = lineRendition; } + SHORT GetId() const noexcept { return _id; } void SetId(const SHORT id) noexcept { _id = id; } @@ -70,6 +74,7 @@ public: private: CharRow _charRow; ATTR_ROW _attrRow; + LineRendition _lineRendition; SHORT _id; unsigned short _rowWidth; // Occurs when the user runs out of text in a given row and we're forced to wrap the cursor to the next line diff --git a/src/buffer/out/lib/bufferout.vcxproj b/src/buffer/out/lib/bufferout.vcxproj index e25fd156d..cd0625dc5 100644 --- a/src/buffer/out/lib/bufferout.vcxproj +++ b/src/buffer/out/lib/bufferout.vcxproj @@ -38,6 +38,7 @@ + diff --git a/src/buffer/out/search.cpp b/src/buffer/out/search.cpp index 40a57c3cb..d91c602c4 100644 --- a/src/buffer/out/search.cpp +++ b/src/buffer/out/search.cpp @@ -97,7 +97,12 @@ bool Search::FindNext() // - Takes the found word and selects it in the screen buffer void Search::Select() const { - _uiaData.SelectNewRegion(_coordSelStart, _coordSelEnd); + // Convert buffer selection offsets into the equivalent screen coordinates + // required by SelectNewRegion, taking line renditions into account. + const auto& textBuffer = _uiaData.GetTextBuffer(); + const auto selStart = textBuffer.BufferToScreenPosition(_coordSelStart); + const auto selEnd = textBuffer.BufferToScreenPosition(_coordSelEnd); + _uiaData.SelectNewRegion(selStart, selEnd); } // Routine Description: @@ -141,7 +146,10 @@ COORD Search::s_GetInitialAnchor(IUiaData& uiaData, const Direction direction) const COORD textBufferEndPosition = uiaData.GetTextBufferEndPosition(); if (uiaData.IsSelectionActive()) { - auto anchor = uiaData.GetSelectionAnchor(); + // Convert the screen position of the selection anchor into an equivalent + // buffer position to start searching, taking line rendition into account. + auto anchor = textBuffer.ScreenToBufferPosition(uiaData.GetSelectionAnchor()); + if (direction == Direction::Forward) { textBuffer.GetSize().IncrementInBoundsCircular(anchor); diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index d649df558..afcda2c92 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -290,13 +290,14 @@ bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute // We only need to compensate for leading bytes if (dbcsAttribute.IsLeading()) { - short const sBufferWidth = GetSize().Width(); + const auto cursorPosition = GetCursor().GetPosition(); + const auto lineWidth = GetLineWidth(cursorPosition.Y); // If we're about to lead on the last column in the row, we need to add a padding space - if (GetCursor().GetPosition().X == sBufferWidth - 1) + if (cursorPosition.X == lineWidth - 1) { // set that we're wrapping for double byte reasons - auto& row = GetRowByOffset(GetCursor().GetPosition().Y); + auto& row = GetRowByOffset(cursorPosition.Y); row.SetDoubleBytePadded(true); // then move the cursor forward and onto the next row @@ -496,7 +497,7 @@ bool TextBuffer::IncrementCursor() // Cursor position is stored as logical array indices (starts at 0) for the window // Buffer Size is specified as the "length" of the array. It would say 80 for valid values of 0-79. // So subtract 1 from buffer size in each direction to find the index of the final column in the buffer - const short iFinalColumnIndex = GetSize().RightInclusive(); + const short iFinalColumnIndex = GetLineWidth(GetCursor().GetPosition().Y) - 1; // Move the cursor one position to the right GetCursor().IncrementXPosition(1); @@ -635,7 +636,7 @@ COORD TextBuffer::GetLastNonSpaceCharacter(std::optional 0) { - // move the cursor to the right edge - coordPosition.X = GetSize().RightInclusive(); - - // and up one line + // move the cursor up one line coordPosition.Y--; + + // and to the right edge + coordPosition.X = GetLineWidth(coordPosition.Y) - 1; } } @@ -801,6 +802,78 @@ void TextBuffer::SetCurrentAttributes(const TextAttribute& currentAttributes) no _currentAttributes = currentAttributes; } +void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition) +{ + const auto cursorPosition = GetCursor().GetPosition(); + const auto rowIndex = cursorPosition.Y; + auto& row = GetRowByOffset(rowIndex); + if (row.GetLineRendition() != lineRendition) + { + row.SetLineRendition(lineRendition); + // If the line rendition has changed, the row can no longer be wrapped. + row.SetWrapForced(false); + // And if it's no longer single width, the right half of the row should be erased. + if (lineRendition != LineRendition::SingleWidth) + { + const auto fillChar = L' '; + auto fillAttrs = GetCurrentAttributes(); + fillAttrs.SetStandardErase(); + const size_t fillOffset = GetLineWidth(rowIndex); + const size_t fillLength = GetSize().Width() - fillOffset; + const auto fillData = OutputCellIterator{ fillChar, fillAttrs, fillLength }; + row.WriteCells(fillData, fillOffset, false); + // We also need to make sure the cursor is clamped within the new width. + GetCursor().SetPosition(ClampPositionWithinLine(cursorPosition)); + } + _NotifyPaint(Viewport::FromDimensions({ 0, rowIndex }, { GetSize().Width(), 1 })); + } +} + +void TextBuffer::ResetLineRenditionRange(const size_t startRow, const size_t endRow) +{ + for (auto row = startRow; row < endRow; row++) + { + GetRowByOffset(row).SetLineRendition(LineRendition::SingleWidth); + } +} + +LineRendition TextBuffer::GetLineRendition(const size_t row) const +{ + return GetRowByOffset(row).GetLineRendition(); +} + +bool TextBuffer::IsDoubleWidthLine(const size_t row) const +{ + return GetLineRendition(row) != LineRendition::SingleWidth; +} + +SHORT TextBuffer::GetLineWidth(const size_t row) const +{ + // Use shift right to quickly divide the width by 2 for double width lines. + const auto scale = IsDoubleWidthLine(row) ? 1 : 0; + return GetSize().Width() >> scale; +} + +COORD TextBuffer::ClampPositionWithinLine(const COORD position) const +{ + const SHORT rightmostColumn = GetLineWidth(position.Y) - 1; + return { std::min(position.X, rightmostColumn), position.Y }; +} + +COORD TextBuffer::ScreenToBufferPosition(const COORD position) const +{ + // Use shift right to quickly divide the X pos by 2 for double width lines. + const auto scale = IsDoubleWidthLine(position.Y) ? 1 : 0; + return { position.X >> scale, position.Y }; +} + +COORD TextBuffer::BufferToScreenPosition(const COORD position) const +{ + // Use shift left to quickly multiply the X pos by 2 for double width lines. + const auto scale = IsDoubleWidthLine(position.Y) ? 1 : 0; + return { position.X << scale, position.Y }; +} + // Routine Description: // - Resets the text contents of this buffer with the default character // and the default current color attributes @@ -1425,9 +1498,11 @@ bool TextBuffer::MoveToPreviousGlyph(til::point& pos) const // - blockSelection: when enabled, only get the rectangular text region, // as opposed to the text extending to the left/right // buffer margins +// - bufferCoordinates: when enabled, treat the coordinates as relative to +// the buffer rather than the screen. // Return Value: // - the delimiter class for the given char -const std::vector TextBuffer::GetTextRects(COORD start, COORD end, bool blockSelection) const +const std::vector TextBuffer::GetTextRects(COORD start, COORD end, bool blockSelection, bool bufferCoordinates) const { std::vector textRects; @@ -1461,6 +1536,13 @@ const std::vector TextBuffer::GetTextRects(COORD start, COORD end, b textRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : bufferSize.RightInclusive(); } + // If we were passed screen coordinates, convert the given range into + // equivalent buffer offsets, taking line rendition into account. + if (!bufferCoordinates) + { + textRow = ScreenToBufferLine(textRow, GetLineRendition(row)); + } + _ExpandTextRow(textRow); textRects.emplace_back(textRow); } @@ -2044,7 +2126,6 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, const COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter(lastCharacterViewport); const short cOldRowsTotal = cOldLastChar.Y + 1; - const short cOldColsTotal = oldBuffer.GetSize().Width(); COORD cNewCursorPos = { 0 }; bool fFoundCursorPos = false; @@ -2056,9 +2137,19 @@ HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, { // Fetch the row and its "right" which is the last printable character. const ROW& row = oldBuffer.GetRowByOffset(iOldRow); + const short cOldColsTotal = oldBuffer.GetLineWidth(iOldRow); const CharRow& charRow = row.GetCharRow(); short iRight = gsl::narrow_cast(charRow.MeasureRight()); + // If we're starting a new row, try and preserve the line rendition + // from the row in the original buffer. + const auto newBufferPos = newBuffer.GetCursor().GetPosition(); + if (newBufferPos.X == 0) + { + auto& newRow = newBuffer.GetRowByOffset(newBufferPos.Y); + newRow.SetLineRendition(row.GetLineRendition()); + } + // There is a special case here. If the row has a "wrap" // flag on it, but the right isn't equal to the width (one // index past the final valid index in the row) then there diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index cfadc5003..d9355e9ab 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -122,6 +122,16 @@ public: void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept; + void SetCurrentLineRendition(const LineRendition lineRendition); + void ResetLineRenditionRange(const size_t startRow, const size_t endRow); + LineRendition GetLineRendition(const size_t row) const; + bool IsDoubleWidthLine(const size_t row) const; + + SHORT GetLineWidth(const size_t row) const; + COORD ClampPositionWithinLine(const COORD position) const; + COORD ScreenToBufferPosition(const COORD position) const; + COORD BufferToScreenPosition(const COORD position) const; + void Reset(); [[nodiscard]] HRESULT ResizeTraditional(const COORD newSize) noexcept; @@ -141,7 +151,7 @@ public: bool MoveToNextGlyph(til::point& pos, bool allowBottomExclusive = false) const; bool MoveToPreviousGlyph(til::point& pos) const; - const std::vector GetTextRects(COORD start, COORD end, bool blockSelection = false) const; + const std::vector GetTextRects(COORD start, COORD end, bool blockSelection, bool bufferCoordinates) const; void AddHyperlinkToMap(std::wstring_view uri, uint16_t id); std::wstring GetHyperlinkUriFromId(uint16_t id) const; @@ -212,7 +222,7 @@ private: void _SetFirstRowIndex(const SHORT FirstRowIndex) noexcept; - COORD _GetPreviousFromCursor() const noexcept; + COORD _GetPreviousFromCursor() const; void _SetWrapOnCurrentRow(); void _AdjustWrapOnCurrentRow(const bool fSet); diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index 30a176a53..f638a9404 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -54,7 +54,7 @@ std::vector Terminal::_GetSelectionRects() const noexcept try { - return _buffer->GetTextRects(_selection->start, _selection->end, _blockSelection); + return _buffer->GetTextRects(_selection->start, _selection->end, _blockSelection, false); } CATCH_LOG(); return result; diff --git a/src/host/CursorBlinker.cpp b/src/host/CursorBlinker.cpp index bfdad623b..0baa8d216 100644 --- a/src/host/CursorBlinker.cpp +++ b/src/host/CursorBlinker.cpp @@ -67,7 +67,8 @@ void CursorBlinker::FocusStart() // - void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo) { - Cursor& cursor = ScreenInfo.GetTextBuffer().GetCursor(); + auto& buffer = ScreenInfo.GetTextBuffer(); + auto& cursor = buffer.GetCursor(); const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto* const _pAccessibilityNotifier = ServiceLocator::LocateAccessibilityNotifier(); @@ -79,7 +80,9 @@ void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo) // Update the cursor pos in USER so accessibility will work. if (cursor.HasMoved()) { - const auto position = cursor.GetPosition(); + // Convert the buffer position to the equivalent screen coordinates + // required by the notifier, taking line rendition into account. + const auto position = buffer.BufferToScreenPosition(cursor.GetPosition()); const auto viewport = ScreenInfo.GetViewport(); const auto fontSize = ScreenInfo.GetScreenFontSize(); cursor.SetHasMoved(false); diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index 00747a596..daef2575d 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -353,7 +353,12 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; const wchar_t* lpString = pwchRealUnicode; - const COORD coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions(); + COORD coordScreenBufferSize = screenInfo.GetBufferSize().Dimensions(); + // In VT mode, the width at which we wrap is determined by the line rendition attribute. + if (WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + { + coordScreenBufferSize.X = textBuffer.GetLineWidth(CursorPosition.Y); + } while (*pcb < BufferSize) { @@ -371,6 +376,11 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100; Status = AdjustCursorPosition(screenInfo, CursorPosition, WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE), psScrollY); CursorPosition = cursor.GetPosition(); + // In VT mode, we need to recalculate the width when moving to a new line. + if (WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + { + coordScreenBufferSize.X = textBuffer.GetLineWidth(CursorPosition.Y); + } } } diff --git a/src/host/conimeinfo.cpp b/src/host/conimeinfo.cpp index 86662775a..4dff990c9 100644 --- a/src/host/conimeinfo.cpp +++ b/src/host/conimeinfo.cpp @@ -422,6 +422,10 @@ void ConsoleImeInfo::_WriteUndeterminedChars(const std::wstring_view text, // screen buffer and viewport positioning. // Each conversion area write will adjust these to set up any subsequent calls to go onto the next line. auto pos = screenInfo.GetTextBuffer().GetCursor().GetPosition(); + // Convert the cursor buffer position to the equivalent screen + // coordinates, taking line rendition into account. + pos = screenInfo.GetTextBuffer().BufferToScreenPosition(pos); + const auto view = screenInfo.GetViewport(); // Set cursor position relative to viewport diff --git a/src/host/getset.cpp b/src/host/getset.cpp index b3ccb9ef7..cbf386889 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -704,13 +704,18 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont buffer.GetViewport().ToInclusive(); COORD delta{ 0 }; { - if (currentViewport.Left > position.X) + // When evaluating the X offset, we must convert the buffer position to + // equivalent screen coordinates, taking line rendition into account. + const auto lineRendition = buffer.GetTextBuffer().GetLineRendition(position.Y); + const auto screenPosition = BufferToScreenLine({ position.X, position.Y, position.X, position.Y }, lineRendition); + + if (currentViewport.Left > screenPosition.Left) { - delta.X = position.X - currentViewport.Left; + delta.X = screenPosition.Left - currentViewport.Left; } - else if (currentViewport.Right < position.X) + else if (currentViewport.Right < screenPosition.Right) { - delta.X = position.X - currentViewport.Right; + delta.X = screenPosition.Right - currentViewport.Right; } if (currentViewport.Top > position.Y) @@ -1410,6 +1415,10 @@ void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool { cursorPosition.X = 0; } + else + { + cursorPosition = textBuffer.ClampPositionWithinLine(cursorPosition); + } return AdjustCursorPosition(screenInfo, cursorPosition, FALSE, nullptr); } @@ -1427,7 +1436,8 @@ void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool const SMALL_RECT viewport = screenInfo.GetActiveBuffer().GetViewport().ToInclusive(); const COORD oldCursorPosition = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - const COORD newCursorPosition = { oldCursorPosition.X, oldCursorPosition.Y - 1 }; + COORD newCursorPosition = { oldCursorPosition.X, oldCursorPosition.Y - 1 }; + newCursorPosition = screenInfo.GetTextBuffer().ClampPositionWithinLine(newCursorPosition); // If the cursor is at the top of the viewport, we don't want to shift the viewport up. // We want it to stay exactly where it is. diff --git a/src/host/output.cpp b/src/host/output.cpp index 29be702d3..c07cb566e 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -451,6 +451,13 @@ void ScrollRegion(SCREEN_INFORMATION& screenInfo, { const auto& view = remaining.at(i); screenInfo.WriteRect(fillData, view); + + // If we're scrolling an area that encompasses the full buffer width, + // then the filled rows should also have their line rendition reset. + if (view.Width() == buffer.Width() && destinationOriginGiven.X == 0) + { + screenInfo.GetTextBuffer().ResetLineRenditionRange(view.Top(), view.BottomExclusive()); + } } } diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index 72bd2e93d..1b04cbcab 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -125,7 +125,9 @@ bool ConhostInternalGetSet::SetConsoleScreenBufferInfoEx(const CONSOLE_SCREEN_BU // - true if successful (see DoSrvSetConsoleCursorPosition). false otherwise. bool ConhostInternalGetSet::SetConsoleCursorPosition(const COORD position) { - return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleCursorPositionImpl(_io.GetActiveOutputBuffer(), position)); + auto& info = _io.GetActiveOutputBuffer(); + const auto clampedPosition = info.GetTextBuffer().ClampPositionWithinLine(position); + return SUCCEEDED(ServiceLocator::LocateGlobals().api.SetConsoleCursorPositionImpl(info, clampedPosition)); } // Routine Description: @@ -182,6 +184,48 @@ bool ConhostInternalGetSet::PrivateSetTextAttributes(const TextAttribute& attrs) return true; } +// Method Description: +// - Sets the line rendition attribute for the current row of the active screen +// buffer. This controls how character cells are scaled when the row is rendered. +// Arguments: +// - lineRendition: The new LineRendition attribute to use +// Return Value: +// - true if successful. false otherwise. +bool ConhostInternalGetSet::PrivateSetCurrentLineRendition(const LineRendition lineRendition) +{ + auto& textBuffer = _io.GetActiveOutputBuffer().GetTextBuffer(); + textBuffer.SetCurrentLineRendition(lineRendition); + return true; +} + +// Method Description: +// - Resets the line rendition attribute to SingleWidth for a specified range +// of row numbers. +// Arguments: +// - startRow: The row number of first line to be modified +// - endRow: The row number following the last line to be modified +// Return Value: +// - true if successful. false otherwise. +bool ConhostInternalGetSet::PrivateResetLineRenditionRange(const size_t startRow, const size_t endRow) +{ + auto& textBuffer = _io.GetActiveOutputBuffer().GetTextBuffer(); + textBuffer.ResetLineRenditionRange(startRow, endRow); + return true; +} + +// Method Description: +// - Returns the number of cells that will fit on the specified row when +// rendered with its current line rendition. +// Arguments: +// - row: The row number of the line to measure +// Return Value: +// - the number of cells that will fit on the line +SHORT ConhostInternalGetSet::PrivateGetLineWidth(const size_t row) const +{ + const auto& textBuffer = _io.GetActiveOutputBuffer().GetTextBuffer(); + return textBuffer.GetLineWidth(row); +} + // Routine Description: // - Connects the WriteConsoleInput API call directly into our Driver Message servicing call inside Conhost.exe // Arguments: diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index a7cc29ae1..b55f80483 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -65,6 +65,10 @@ public: bool PrivateGetTextAttributes(TextAttribute& attrs) const override; bool PrivateSetTextAttributes(const TextAttribute& attrs) override; + bool PrivateSetCurrentLineRendition(const LineRendition lineRendition) override; + bool PrivateResetLineRenditionRange(const size_t startRow, const size_t endRow) override; + SHORT PrivateGetLineWidth(const size_t row) const override; + bool PrivateWriteConsoleInputW(std::deque>& events, size_t& eventsWritten) override; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 919460759..31f36d746 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -2223,6 +2223,9 @@ void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport, auto fillData = OutputCellIterator{ fillAttributes, fillLength }; Write(fillData, fillPosition, false); + // Also reset the line rendition for the erased rows. + _textBuffer->ResetLineRenditionRange(_viewport.Top(), _viewport.BottomExclusive()); + return S_OK; } @@ -2395,7 +2398,9 @@ void SCREEN_INFORMATION::ClearTextData() // - word boundary positions std::pair SCREEN_INFORMATION::GetWordBoundary(const COORD position) const { - COORD clampedPosition = position; + // The position argument is in screen coordinates, but we need the + // equivalent buffer position, taking line rendition into account. + COORD clampedPosition = _textBuffer->ScreenToBufferPosition(position); GetBufferSize().Clamp(clampedPosition); COORD start{ clampedPosition }; @@ -2464,6 +2469,12 @@ std::pair SCREEN_INFORMATION::GetWordBoundary(const COORD position } } } + + // The calculated range is in buffer coordinates, but the caller is + // expecting screen offsets, so we have to convert these back again. + start = _textBuffer->BufferToScreenPosition(start); + end = _textBuffer->BufferToScreenPosition(end); + return { start, end }; } @@ -2541,6 +2552,8 @@ void SCREEN_INFORMATION::InitializeCursorRowAttributes() auto fillAttributes = GetAttributes(); fillAttributes.SetStandardErase(); row.GetAttrRow().SetAttrToEnd(0, fillAttributes); + // The row should also be single width to start with. + row.SetLineRendition(LineRendition::SingleWidth); } } diff --git a/src/host/selection.cpp b/src/host/selection.cpp index 7da4ee0cf..77f7a3ca0 100644 --- a/src/host/selection.cpp +++ b/src/host/selection.cpp @@ -59,7 +59,7 @@ std::vector Selection::GetSelectionRects() const endSelectionAnchor.Y = (_coordSelectionAnchor.Y == _srSelectionRect.Top) ? _srSelectionRect.Bottom : _srSelectionRect.Top; const auto blockSelection = !IsLineSelection(); - return screenInfo.GetTextBuffer().GetTextRects(_coordSelectionAnchor, endSelectionAnchor, blockSelection); + return screenInfo.GetTextBuffer().GetTextRects(_coordSelectionAnchor, endSelectionAnchor, blockSelection, false); } // Routine Description: @@ -417,7 +417,7 @@ void Selection::ColorSelection(const COORD coordSelectionStart, const COORD coor const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto& screenInfo = gci.GetActiveOutputBuffer(); - const auto rectangles = screenInfo.GetTextBuffer().GetTextRects(coordSelectionStart, coordSelectionEnd); + const auto rectangles = screenInfo.GetTextBuffer().GetTextRects(coordSelectionStart, coordSelectionEnd, false, true); for (const auto& rect : rectangles) { ColorSelection(rect, attr); diff --git a/src/host/ut_host/SelectionTests.cpp b/src/host/ut_host/SelectionTests.cpp index dcdaa7765..80515ceb6 100644 --- a/src/host/ut_host/SelectionTests.cpp +++ b/src/host/ut_host/SelectionTests.cpp @@ -333,7 +333,7 @@ class SelectionTests COORD startPos{ sTargetX, sTargetY }; COORD endPos{ base::ClampAdd(sTargetX, sLength), sTargetY }; - const auto selectionRects = screenInfo.GetTextBuffer().GetTextRects(startPos, endPos); + const auto selectionRects = screenInfo.GetTextBuffer().GetTextRects(startPos, endPos, false, false); VERIFY_ARE_EQUAL(static_cast(1), selectionRects.size()); srSelection = selectionRects.at(0); diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 3c9e7f3fc..c87c740fd 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -2324,7 +2324,7 @@ void TextBufferTests::GetTextRects() COORD start{ 1, 0 }; COORD end{ 7, 4 }; - const auto result = _buffer->GetTextRects(start, end, blockSelection); + const auto result = _buffer->GetTextRects(start, end, blockSelection, false); VERIFY_ARE_EQUAL(expected.size(), result.size()); for (size_t i = 0; i < expected.size(); ++i) { @@ -2370,7 +2370,7 @@ void TextBufferTests::GetText() WriteLinesToBuffer(bufferText, *_buffer); // simulate a selection from origin to {4,4} - const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 4 }, blockSelection); + const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 4 }, blockSelection, false); std::wstring result = L""; const auto textData = _buffer->GetText(includeCRLF, trimTrailingWhitespace, textRects).text; @@ -2471,7 +2471,7 @@ void TextBufferTests::GetText() // |_____| // simulate a selection from origin to {4,5} - const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 5 }, blockSelection); + const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 5 }, blockSelection, false); std::wstring result = L""; diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index 9a8d03fde..cf690ecd3 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -38,7 +38,7 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid return S_OK; } -[[nodiscard]] HRESULT BgfxEngine::InvalidateCursor(const COORD* const /*pcoordCursor*/) noexcept +[[nodiscard]] HRESULT BgfxEngine::InvalidateCursor(const SMALL_RECT* const /*psrRegion*/) noexcept { return S_OK; } diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 542daa6e5..4fddcfb0b 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -34,7 +34,7 @@ namespace Microsoft::Console::Render // IRenderEngine Members [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; - [[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) 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; diff --git a/src/renderer/base/RenderEngineBase.cpp b/src/renderer/base/RenderEngineBase.cpp index 9830e4871..f31fff625 100644 --- a/src/renderer/base/RenderEngineBase.cpp +++ b/src/renderer/base/RenderEngineBase.cpp @@ -41,6 +41,18 @@ HRESULT RenderEngineBase::PrepareRenderInfo(const RenderFrameInfo& /*info*/) noe return S_FALSE; } +HRESULT RenderEngineBase::ResetLineTransform() noexcept +{ + return S_FALSE; +} + +HRESULT RenderEngineBase::PrepareLineTransform(const LineRendition /*lineRendition*/, + const size_t /*targetRow*/, + const size_t /*viewportLeft*/) noexcept +{ + return S_FALSE; +} + // Method Description: // - By default, no one should need continuous redraw. It ruins performance // in terms of CPU, memory, and battery life to just paint forever. diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 31a9d2f69..862821db4 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -220,6 +220,18 @@ 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)) + { + srUpdateRegion.Right *= 2; + break; + } + } + if (view.TrimToViewport(&srUpdateRegion)) { view.ConvertToOrigin(&srUpdateRegion); @@ -253,25 +265,34 @@ void Renderer::TriggerRedraw(const COORD* const pcoord) // - void Renderer::TriggerRedrawCursor(const COORD* const pcoord) { - Viewport view = _pData->GetViewport(); - COORD updateCoord = *pcoord; - - if (view.IsInBounds(updateCoord)) + // We first need to make sure the cursor position is within the buffer, + // otherwise testing for a double width character can throw an exception. + const auto& buffer = _pData->GetTextBuffer(); + if (buffer.GetSize().IsInBounds(*pcoord)) { - view.ConvertToOrigin(&updateCoord); - for (IRenderEngine* pEngine : _rgpEngines) + // We then calculate the region covered by the cursor. This requires + // converting the buffer coordinates to an equivalent range of screen + // cells for the cursor, taking line rendition into account. + const LineRendition lineRendition = buffer.GetLineRendition(pcoord->Y); + const SHORT cursorWidth = _pData->IsCursorDoubleWidth() ? 2 : 1; + const SMALL_RECT cursorRect = { pcoord->X, pcoord->Y, pcoord->X + cursorWidth - 1, pcoord->Y }; + Viewport cursorView = Viewport::FromInclusive(BufferToScreenLine(cursorRect, lineRendition)); + + // The region is clamped within the viewport boundaries and we only + // trigger a redraw if the region is not empty. + Viewport view = _pData->GetViewport(); + cursorView = view.Clamp(cursorView); + + if (cursorView.IsValid()) { - LOG_IF_FAILED(pEngine->InvalidateCursor(&updateCoord)); - - // Double-wide cursors need to invalidate the right half as well. - if (_pData->IsCursorDoubleWidth()) + const SMALL_RECT updateRect = view.ConvertToOrigin(cursorView).ToExclusive(); + for (IRenderEngine* pEngine : _rgpEngines) { - updateCoord.X++; - LOG_IF_FAILED(pEngine->InvalidateCursor(&updateCoord)); + LOG_IF_FAILED(pEngine->InvalidateCursor(&updateRect)); } - } - _NotifyPaintFrame(); + _NotifyPaintFrame(); + } } } @@ -630,6 +651,11 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) 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); @@ -654,14 +680,19 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) { // Calculate the boundaries of a single line. This is from the left to right edge of the dirty // area in width and exactly 1 tall. - const auto bufferLine = Viewport::FromDimensions({ redraw.Left(), row }, { redraw.Width(), 1 }); + 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 1,1 because it is dirty but the viewport is actually looking - // at 13,26 relative to the buffer. - // This means that we need 14,27 out of the backing buffer to fill in the 1,1 cell of the screen. - const auto screenLine = Viewport::Offset(bufferLine, -view.Origin()); + // 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); @@ -673,8 +704,11 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) 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, screenLine.Origin(), lineWrapped); + _PaintBufferOutputHelper(pEngine, it, screenPosition, lineWrapped); } } } @@ -959,11 +993,25 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin // 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. - Viewport view = _pData->GetViewport(); - if (view.IsInBounds(coordCursor)) + + // 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 to viewport - view.ConvertToOrigin(&coordCursor); + // 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; @@ -971,6 +1019,8 @@ void Renderer::_PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngin // 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(); @@ -1165,14 +1215,20 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine) // - 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) + 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. diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index a66d312e9..b124c621f 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1055,24 +1055,15 @@ try CATCH_RETURN() // Routine Description: -// - Invalidates one specific character coordinate +// - Invalidates the cells of the cursor // Arguments: -// - pcoordCursor - single point in the character cell grid +// - psrRegion - the region covered by the cursor // Return Value: // - S_OK -[[nodiscard]] HRESULT DxEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept -try +[[nodiscard]] HRESULT DxEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept { - RETURN_HR_IF_NULL(E_INVALIDARG, pcoordCursor); - - if (!_allInvalid) - { - _InvalidateRectangle(til::rectangle{ *pcoordCursor, til::size{ 1, 1 } }); - } - - return S_OK; + return Invalidate(psrRegion); } -CATCH_RETURN() // Routine Description: // - Invalidates a rectangle describing a pixel area on the display diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 344dedec3..97f92f8cf 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -73,7 +73,7 @@ namespace Microsoft::Console::Render // IRenderEngine Members [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; - [[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) 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; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 3038675ef..230b09c4f 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -30,7 +30,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT InvalidateScroll(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 COORD* const pcoordCursor) noexcept override; + [[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) 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; @@ -41,6 +41,11 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT ScrollFrame() noexcept override; + [[nodiscard]] HRESULT ResetLineTransform() noexcept override; + [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, + const size_t targetRow, + const size_t viewportLeft) noexcept override; + [[nodiscard]] HRESULT PaintBackground() noexcept override; [[nodiscard]] HRESULT PaintBufferLine(gsl::span const clusters, const COORD coord, @@ -97,6 +102,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _FlushBufferLines() noexcept; std::vector cursorInvertRects; + XFORM cursorInvertTransform; struct LineMetrics { @@ -126,6 +132,9 @@ namespace Microsoft::Console::Render COLORREF _lastBg; bool _lastFontItalic; + XFORM _currentLineTransform; + LineRendition _currentLineRendition; + // Memory pooling to save alloc/free work to the OS for things // frequently created and dropped. // It's important the pool is first so it can be given to the others on construction. @@ -183,4 +192,11 @@ namespace Microsoft::Console::Render HDC _debugContext; #endif }; + + constexpr XFORM IDENTITY_XFORM = { 1, 0, 0, 1 }; + + inline bool operator==(const XFORM& lhs, const XFORM& rhs) noexcept + { + return ::memcmp(&lhs, &rhs, sizeof(XFORM)) == 0; + }; } diff --git a/src/renderer/gdi/invalidate.cpp b/src/renderer/gdi/invalidate.cpp index ff68b5e17..ca77b03cd 100644 --- a/src/renderer/gdi/invalidate.cpp +++ b/src/renderer/gdi/invalidate.cpp @@ -81,13 +81,12 @@ HRESULT GdiEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept // Routine Description: // - Notifies us that the console has changed the position of the cursor. // Arguments: -// - pcoordCursor - the new position of the cursor +// - psrRegion - the region covered by the cursor // Return Value: // - S_OK, else an appropriate HRESULT for failing to allocate or write. -HRESULT GdiEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept +HRESULT GdiEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept { - SMALL_RECT sr = Viewport::FromCoord(*pcoordCursor).ToExclusive(); - return this->Invalidate(&sr); + return this->Invalidate(psrRegion); } // Routine Description: diff --git a/src/renderer/gdi/paint.cpp b/src/renderer/gdi/paint.cpp index 61e8e05ec..9295d3d6d 100644 --- a/src/renderer/gdi/paint.cpp +++ b/src/renderer/gdi/paint.cpp @@ -41,6 +41,9 @@ using namespace Microsoft::Console::Render; _psInvalidData.hdc = GetDC(_hwndTargetWindow); RETURN_HR_IF_NULL(E_FAIL, _psInvalidData.hdc); + // We need the advanced graphics mode in order to set a transform. + SetGraphicsMode(_psInvalidData.hdc, GM_ADVANCED); + // Signal that we're starting to paint. _fPaintStarted = true; @@ -71,11 +74,28 @@ using namespace Microsoft::Console::Render; // left behind cursor copies in the scrolled region. if (cursorInvertRects.size() > 0) { + // We first need to apply the transform that was active at the time the cursor + // was rendered otherwise we won't be clearing the right area of the display. + // We don't need to do this if it was an identity transform though. + const bool identityTransform = cursorInvertTransform == IDENTITY_XFORM; + if (!identityTransform) + { + LOG_HR_IF(E_FAIL, !SetWorldTransform(_hdcMemoryContext, &cursorInvertTransform)); + LOG_HR_IF(E_FAIL, !SetWorldTransform(_psInvalidData.hdc, &cursorInvertTransform)); + } + for (RECT r : cursorInvertRects) { // Clean both the in-memory and actual window context. - RETURN_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r))); - RETURN_HR_IF(E_FAIL, !(InvertRect(_psInvalidData.hdc, &r))); + LOG_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r))); + LOG_HR_IF(E_FAIL, !(InvertRect(_psInvalidData.hdc, &r))); + } + + // If we've applied a transform, then we need to reset it. + if (!identityTransform) + { + LOG_HR_IF(E_FAIL, !ModifyWorldTransform(_hdcMemoryContext, nullptr, MWT_IDENTITY)); + LOG_HR_IF(E_FAIL, !ModifyWorldTransform(_psInvalidData.hdc, nullptr, MWT_IDENTITY)); } cursorInvertRects.clear(); @@ -369,15 +389,21 @@ using namespace Microsoft::Console::Render; } } + // If the line rendition is double height, we need to adjust the top or bottom + // of the clipping rect to clip half the height of the rendered characters. + const auto halfHeight = coordFontSize.Y >> 1; + const auto topOffset = _currentLineRendition == LineRendition::DoubleHeightBottom ? halfHeight : 0; + const auto bottomOffset = _currentLineRendition == LineRendition::DoubleHeightTop ? halfHeight : 0; + pPolyTextLine->lpstr = polyString.data(); pPolyTextLine->n = gsl::narrow(clusters.size()); pPolyTextLine->x = ptDraw.x; pPolyTextLine->y = ptDraw.y; pPolyTextLine->uiFlags = ETO_OPAQUE | ETO_CLIPPED; pPolyTextLine->rcl.left = pPolyTextLine->x; - pPolyTextLine->rcl.top = pPolyTextLine->y; - pPolyTextLine->rcl.right = pPolyTextLine->rcl.left + ((SHORT)cchCharWidths * coordFontSize.X); - pPolyTextLine->rcl.bottom = pPolyTextLine->rcl.top + coordFontSize.Y; + pPolyTextLine->rcl.top = pPolyTextLine->y + topOffset; + pPolyTextLine->rcl.right = pPolyTextLine->rcl.left + (SHORT)cchCharWidths; + pPolyTextLine->rcl.bottom = pPolyTextLine->y + coordFontSize.Y - bottomOffset; pPolyTextLine->pdx = polyWidth.data(); if (trimLeft) @@ -628,6 +654,13 @@ using namespace Microsoft::Console::Render; default: return E_NOTIMPL; } + + // 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()); + }); + // Either invert all the RECTs, or paint them. if (options.fUseColor) { @@ -642,6 +675,10 @@ using namespace Microsoft::Console::Render; } else { + // Save the current line transform in case we need to reapply these + // inverted rects to hide the cursor in the ScrollFrame method. + cursorInvertTransform = _currentLineTransform; + for (RECT r : cursorInvertRects) { RETURN_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r))); diff --git a/src/renderer/gdi/state.cpp b/src/renderer/gdi/state.cpp index 81fba42d6..3311e7585 100644 --- a/src/renderer/gdi/state.cpp +++ b/src/renderer/gdi/state.cpp @@ -30,6 +30,8 @@ GdiEngine::GdiEngine() : _lastFg(INVALID_COLOR), _lastBg(INVALID_COLOR), _lastFontItalic(false), + _currentLineTransform(IDENTITY_XFORM), + _currentLineRendition(LineRendition::SingleWidth), _fPaintStarted(false), _invalidCharacters{}, _hfont(nullptr), @@ -46,6 +48,9 @@ GdiEngine::GdiEngine() : _hdcMemoryContext = CreateCompatibleDC(nullptr); THROW_HR_IF_NULL(E_FAIL, _hdcMemoryContext); + // We need the advanced graphics mode in order to set a transform. + SetGraphicsMode(_hdcMemoryContext, GM_ADVANCED); + // On session zero, text GDI APIs might not be ready. // Calling GetTextFace causes a wait that will be // satisfied while GDI text APIs come online. @@ -123,6 +128,9 @@ GdiEngine::~GdiEngine() HDC const hdcNewMemoryContext = CreateCompatibleDC(hdcRealWindow); RETURN_HR_IF_NULL(E_FAIL, hdcNewMemoryContext); + // We need the advanced graphics mode in order to set a transform. + SetGraphicsMode(hdcNewMemoryContext, GM_ADVANCED); + // If we had an existing memory context stored, release it before proceeding. if (nullptr != _hdcMemoryContext) { @@ -185,6 +193,77 @@ GdiEngine::~GdiEngine() return S_OK; } +// Routine Description +// - Resets the world transform to the identity matrix. +// Arguments: +// - +// Return Value: +// - S_OK if successful. S_FALSE if already reset. E_FAIL if there was an error. +[[nodiscard]] HRESULT GdiEngine::ResetLineTransform() noexcept +{ + // Return early if the current transform is already the identity matrix. + RETURN_HR_IF(S_FALSE, _currentLineTransform == IDENTITY_XFORM); + // Flush any buffer lines which would be expecting to use the current transform. + LOG_IF_FAILED(_FlushBufferLines()); + // Reset the active transform to the identity matrix. + RETURN_HR_IF(E_FAIL, !ModifyWorldTransform(_hdcMemoryContext, nullptr, MWT_IDENTITY)); + // Reset the current state. + _currentLineTransform = IDENTITY_XFORM; + _currentLineRendition = LineRendition::SingleWidth; + return S_OK; +} + +// Routine Description +// - Applies an appropriate transform for the given line rendition and viewport offset. +// Arguments: +// - lineRendition - The line rendition specifying the scaling of the line. +// - targetRow - The row on which the line is expected to be rendered. +// - viewportLeft - The left offset of the current viewport. +// Return Value: +// - S_OK if successful. S_FALSE if already set. E_FAIL if there was an error. +[[nodiscard]] HRESULT GdiEngine::PrepareLineTransform(const LineRendition lineRendition, + const size_t targetRow, + const size_t viewportLeft) noexcept +{ + XFORM lineTransform = {}; + // The X delta is to account for the horizontal viewport offset. + lineTransform.eDx = viewportLeft ? -1.0f * viewportLeft * _GetFontSize().X : 0.0f; + switch (lineRendition) + { + case LineRendition::SingleWidth: + lineTransform.eM11 = 1; // single width + lineTransform.eM22 = 1; // single height + break; + case LineRendition::DoubleWidth: + lineTransform.eM11 = 2; // double width + lineTransform.eM22 = 1; // single height + break; + case LineRendition::DoubleHeightTop: + lineTransform.eM11 = 2; // double width + lineTransform.eM22 = 2; // double height + // The Y delta is to negate the offset caused by the scaled height. + lineTransform.eDy = -1.0f * targetRow * _GetFontSize().Y; + break; + case LineRendition::DoubleHeightBottom: + lineTransform.eM11 = 2; // double width + lineTransform.eM22 = 2; // double height + // The Y delta is to negate the offset caused by the scaled height. + // An extra row is added because we need the bottom half of the line. + lineTransform.eDy = -1.0f * (targetRow + 1) * _GetFontSize().Y; + break; + } + // Return early if the new matrix is the same as the current transform. + RETURN_HR_IF(S_FALSE, _currentLineRendition == lineRendition && _currentLineTransform == lineTransform); + // Flush any buffer lines which would be expecting to use the current transform. + LOG_IF_FAILED(_FlushBufferLines()); + // Set the active transform with the new matrix. + RETURN_HR_IF(E_FAIL, !SetWorldTransform(_hdcMemoryContext, &lineTransform)); + // Save the current state. + _currentLineTransform = lineTransform; + _currentLineRendition = lineRendition; + return S_OK; +} + // Routine Description: // - This method will set the GDI brushes in the drawing context (and update the hung-window background color) // Arguments: diff --git a/src/renderer/inc/CursorOptions.h b/src/renderer/inc/CursorOptions.h index 27ddc795c..4fa5c6b89 100644 --- a/src/renderer/inc/CursorOptions.h +++ b/src/renderer/inc/CursorOptions.h @@ -14,6 +14,7 @@ Author(s): #pragma once +#include "../../buffer/out/LineRendition.hpp" #include "../../inc/conattrs.hpp" namespace Microsoft::Console::Render @@ -21,9 +22,15 @@ namespace Microsoft::Console::Render struct CursorOptions { // Character cell in the grid to draw at - // This is relative to the viewport, not the buffer. + // This is relative to the top of the viewport, not the buffer COORD coordCursor; + // Left offset of the viewport, which may alter the horizontal position + SHORT viewportLeft; + + // Line rendition of the current row, which can affect the cursor width + LineRendition lineRendition; + // For an underscore type _ cursor, how tall it should be as a % of cell height ULONG ulCursorHeightPercent; diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 688df2f79..3d6631e41 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -18,6 +18,7 @@ Author(s): #include "Cluster.hpp" #include "FontInfoDesired.hpp" #include "IRenderData.hpp" +#include "../../buffer/out/LineRendition.hpp" namespace Microsoft::Console::Render { @@ -64,7 +65,7 @@ namespace Microsoft::Console::Render [[nodiscard]] virtual HRESULT ScrollFrame() noexcept = 0; [[nodiscard]] virtual HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept = 0; - [[nodiscard]] virtual HRESULT InvalidateCursor(const COORD* const pcoordCursor) 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; @@ -75,6 +76,11 @@ namespace Microsoft::Console::Render [[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, diff --git a/src/renderer/inc/RenderEngineBase.hpp b/src/renderer/inc/RenderEngineBase.hpp index 378a6a56d..725347d82 100644 --- a/src/renderer/inc/RenderEngineBase.hpp +++ b/src/renderer/inc/RenderEngineBase.hpp @@ -40,6 +40,11 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT PrepareRenderInfo(const RenderFrameInfo& info) noexcept override; + [[nodiscard]] HRESULT ResetLineTransform() noexcept override; + [[nodiscard]] HRESULT PrepareLineTransform(const LineRendition lineRendition, + const size_t targetRow, + const size_t viewportLeft) noexcept override; + [[nodiscard]] virtual bool RequiresContinuousRedraw() noexcept override; void WaitUntilCanRender() noexcept override; diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index f3a848fa7..ba7ca8e5f 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -21,6 +21,7 @@ UiaEngine::UiaEngine(IUiaEventDispatcher* dispatcher) : _cursorChanged{ false }, _isEnabled{ true }, _prevSelection{}, + _prevCursorRegion{}, RenderEngineBase() { } @@ -66,18 +67,18 @@ UiaEngine::UiaEngine(IUiaEventDispatcher* dispatcher) : // - Notifies us that the console has changed the position of the cursor. // For UIA, this doesn't mean anything. So do nothing. // Arguments: -// - pcoordCursor - the new position of the cursor +// - psrRegion - the region covered by the cursor // Return Value: // - S_OK -[[nodiscard]] HRESULT UiaEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept +[[nodiscard]] HRESULT UiaEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept try { - RETURN_HR_IF_NULL(E_INVALIDARG, pcoordCursor); + RETURN_HR_IF_NULL(E_INVALIDARG, psrRegion); // check if cursor moved - if (*pcoordCursor != _prevCursorPos) + if (*psrRegion != _prevCursorRegion) { - _prevCursorPos = *pcoordCursor; + _prevCursorRegion = *psrRegion; _cursorChanged = true; } return S_OK; diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index 23b092c6e..3ead4673b 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -43,7 +43,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT ScrollFrame() noexcept override; [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; - [[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) 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; @@ -86,6 +86,6 @@ namespace Microsoft::Console::Render Microsoft::Console::Types::IUiaEventDispatcher* _dispatcher; std::vector _prevSelection; - til::point _prevCursorPos; + SMALL_RECT _prevCursorRegion; }; } diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp index 7e85a81cf..1856ac4cd 100644 --- a/src/renderer/vt/invalidate.cpp +++ b/src/renderer/vt/invalidate.cpp @@ -59,10 +59,10 @@ CATCH_RETURN(); // Routine Description: // - Notifies us that the console has changed the position of the cursor. // Arguments: -// - pcoordCursor - the new position of the cursor +// - psrRegion - the region covered by the cursor // Return Value: // - S_OK -[[nodiscard]] HRESULT VtEngine::InvalidateCursor(const COORD* const pcoordCursor) noexcept +[[nodiscard]] HRESULT VtEngine::InvalidateCursor(const SMALL_RECT* const psrRegion) noexcept { // If we just inherited the cursor, we're going to get an InvalidateCursor // for both where the old cursor was, and where the new cursor is @@ -70,9 +70,9 @@ CATCH_RETURN(); // We should ignore the first one, but after that, if the client application // is moving the cursor around in the viewport, move our virtual top // up to meet their changes. - if (!_skipCursor && _virtualTop > pcoordCursor->Y) + if (!_skipCursor && _virtualTop > psrRegion->Top) { - _virtualTop = pcoordCursor->Y; + _virtualTop = psrRegion->Top; } _skipCursor = false; diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index ad6c36d35..a6d8e60f2 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -49,7 +49,7 @@ namespace Microsoft::Console::Render [[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 COORD* const pcoordCursor) noexcept override; + [[nodiscard]] HRESULT InvalidateCursor(const SMALL_RECT* const psrRegion) 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; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index 35415e53c..ed2c027ee 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -166,7 +166,7 @@ bool WddmConEngine::IsInitialized() return S_OK; } -[[nodiscard]] HRESULT WddmConEngine::InvalidateCursor(const COORD* const /*pcoordCursor*/) noexcept +[[nodiscard]] HRESULT WddmConEngine::InvalidateCursor(const SMALL_RECT* const /*psrRegion*/) noexcept { return S_OK; } diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index 0d142be1b..6898f1ece 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -26,7 +26,7 @@ namespace Microsoft::Console::Render // IRenderEngine Members [[nodiscard]] HRESULT Invalidate(const SMALL_RECT* const psrRegion) noexcept override; - [[nodiscard]] HRESULT InvalidateCursor(const COORD* const pcoordCursor) 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; diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index e18f0cd46..fe53e49d8 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -13,6 +13,7 @@ Abstract: */ #pragma once #include "DispatchTypes.hpp" +#include "../buffer/out/LineRendition.hpp" namespace Microsoft::Console::VirtualTerminal { @@ -88,6 +89,7 @@ public: virtual bool EraseCharacters(const size_t numChars) = 0; // ECH virtual bool SetGraphicsRendition(const VTParameters options) = 0; // SGR + virtual bool SetLineRendition(const LineRendition rendition) = 0; // DECSWL, DECDWL, DECDHL virtual bool PushGraphicsRendition(const VTParameters options) = 0; // XTPUSHSGR virtual bool PopGraphicsRendition() = 0; // XTPOPSGR diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 5707edd91..fade27e61 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -432,10 +432,9 @@ bool AdaptDispatch::_InsertDeleteHelper(const size_t count, const bool isInsert) const auto cursor = csbiex.dwCursorPosition; // Rectangle to cut out of the existing buffer. This is inclusive. - // It will be clipped to the buffer boundaries so SHORT_MAX gives us the full buffer width. SMALL_RECT srScroll; srScroll.Left = cursor.X; - srScroll.Right = SHORT_MAX; + srScroll.Right = _pConApi->PrivateGetLineWidth(cursor.Y) - 1; srScroll.Top = cursor.Y; srScroll.Bottom = srScroll.Top; @@ -534,7 +533,7 @@ bool AdaptDispatch::_EraseSingleLineHelper(const CONSOLE_SCREEN_BUFFER_INFOEX& c case DispatchTypes::EraseType::ToEnd: case DispatchTypes::EraseType::All: // Remember the .X value is 1 farther than the right most column in the buffer. Therefore no +1. - nLength = csbiex.dwSize.X - coordStartPosition.X; + nLength = _pConApi->PrivateGetLineWidth(lineId) - coordStartPosition.X; break; } @@ -622,6 +621,23 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) if (success) { + // When erasing the display, every line that is erased in full should be + // reset to single width. When erasing to the end, this could include + // the current line, if the cursor is in the first column. When erasing + // from the beginning, though, the current line would never be included, + // because the cursor could never be in the rightmost column (assuming + // the line is double width). + if (eraseType == DispatchTypes::EraseType::FromBeginning) + { + const auto endRow = csbiex.dwCursorPosition.Y; + _pConApi->PrivateResetLineRenditionRange(csbiex.srWindow.Top, endRow); + } + if (eraseType == DispatchTypes::EraseType::ToEnd) + { + const auto startRow = csbiex.dwCursorPosition.Y + (csbiex.dwCursorPosition.X > 0 ? 1 : 0); + _pConApi->PrivateResetLineRenditionRange(startRow, csbiex.srWindow.Bottom); + } + // What we need to erase is grouped into 3 types: // 1. Lines before cursor // 2. Cursor Line @@ -697,6 +713,18 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) return success; } +// Routine Description: +// - DECSWL/DECDWL/DECDHL - Sets the line rendition attribute for the current line. +// Arguments: +// - rendition - Determines whether the line will be rendered as single width, double +// width, or as one half of a double height line. +// Return Value: +// - True if handled successfully. False otherwise. +bool AdaptDispatch::SetLineRendition(const LineRendition rendition) +{ + return _pConApi->PrivateSetCurrentLineRendition(rendition); +} + // Routine Description: // - DSR - Reports status of a console property back to the STDIN based on the type of status requested. // - This particular routine responds to ANSI status patterns only (CSI # n), not the DEC format (CSI ? # n) @@ -1931,6 +1959,8 @@ bool AdaptDispatch::ScreenAlignmentPattern() auto fillPosition = COORD{ 0, csbiex.srWindow.Top }; const auto fillLength = (csbiex.srWindow.Bottom - csbiex.srWindow.Top) * csbiex.dwSize.X; success = _pConApi->PrivateFillRegion(fillPosition, fillLength, L'E', false); + // Reset the line rendition for all of these rows. + success = success && _pConApi->PrivateResetLineRenditionRange(csbiex.srWindow.Top, csbiex.srWindow.Bottom); // Reset the meta/extended attributes (but leave the colors unchanged). TextAttribute attr; if (_pConApi->PrivateGetTextAttributes(attr)) @@ -1995,6 +2025,8 @@ bool AdaptDispatch::_EraseScrollback() const COORD coordBelowStartPosition = { 0, height }; // Again we need to use the default attributes, hence standardFillAttrs is false. success = _pConApi->PrivateFillRegion(coordBelowStartPosition, totalAreaBelow, L' ', false); + // Also reset the line rendition for all of the cleared rows. + success = success && _pConApi->PrivateResetLineRenditionRange(height, csbiex.dwSize.Y); if (success) { diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 77f17f5b9..5ea582343 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -57,6 +57,7 @@ namespace Microsoft::Console::VirtualTerminal bool InsertCharacter(const size_t count) override; // ICH bool DeleteCharacter(const size_t count) override; // DCH bool SetGraphicsRendition(const VTParameters options) override; // SGR + bool SetLineRendition(const LineRendition rendition) override; // DECSWL, DECDWL, DECDHL bool PushGraphicsRendition(const VTParameters options) override; // XTPUSHSGR bool PopGraphicsRendition() override; // XTPOPSGR bool DeviceStatusReport(const DispatchTypes::AnsiStatusType statusType) override; // DSR, DSR-OS, DSR-CPR diff --git a/src/terminal/adapter/conGetSet.hpp b/src/terminal/adapter/conGetSet.hpp index 04049348d..ba93c100d 100644 --- a/src/terminal/adapter/conGetSet.hpp +++ b/src/terminal/adapter/conGetSet.hpp @@ -16,6 +16,7 @@ Author(s): #pragma once #include "../../types/inc/IInputEvent.hpp" +#include "../../buffer/out/LineRendition.hpp" #include "../../buffer/out/TextAttribute.hpp" #include "../../inc/conattrs.hpp" @@ -39,6 +40,10 @@ namespace Microsoft::Console::VirtualTerminal virtual bool PrivateGetTextAttributes(TextAttribute& attrs) const = 0; virtual bool PrivateSetTextAttributes(const TextAttribute& attrs) = 0; + virtual bool PrivateSetCurrentLineRendition(const LineRendition lineRendition) = 0; + virtual bool PrivateResetLineRenditionRange(const size_t startRow, const size_t endRow) = 0; + virtual SHORT PrivateGetLineWidth(const size_t row) const = 0; + virtual bool PrivateWriteConsoleInputW(std::deque>& events, size_t& eventsWritten) = 0; virtual bool SetConsoleWindowInfo(const bool absolute, diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 3fb4b0d2d..d82a28c22 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -82,6 +82,7 @@ public: bool EraseCharacters(const size_t /*numChars*/) noexcept override { return false; } // ECH bool SetGraphicsRendition(const VTParameters /*options*/) noexcept override { return false; } // SGR + bool SetLineRendition(const LineRendition /*rendition*/) noexcept override { return false; } // DECSWL, DECDWL, DECDHL bool PushGraphicsRendition(const VTParameters /*options*/) noexcept override { return false; } // XTPUSHSGR bool PopGraphicsRendition() noexcept override { return false; } // XTPOPSGR diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 577383311..b2b1a2be2 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -249,6 +249,27 @@ public: return _privateSetTextAttributesResult; } + bool PrivateSetCurrentLineRendition(const LineRendition /*lineRendition*/) + { + Log::Comment(L"PrivateSetCurrentLineRendition MOCK called..."); + + return false; + } + + bool PrivateResetLineRenditionRange(const size_t /*startRow*/, const size_t /*endRow*/) + { + Log::Comment(L"PrivateResetLineRenditionRange MOCK called..."); + + return false; + } + + SHORT PrivateGetLineWidth(const size_t /*row*/) const + { + Log::Comment(L"PrivateGetLineWidth MOCK called..."); + + return _bufferSize.X; + } + bool PrivateWriteConsoleInputW(std::deque>& events, size_t& eventsWritten) override { diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index a214f5fd1..cdc753a7c 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -265,6 +265,22 @@ bool OutputStateMachineEngine::ActionEscDispatch(const VTID id) success = _dispatch->LockingShiftRight(3); TermTelemetry::Instance().Log(TermTelemetry::Codes::LS3R); break; + case EscActionCodes::DECDHL_DoubleHeightLineTop: + _dispatch->SetLineRendition(LineRendition::DoubleHeightTop); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDHL); + break; + case EscActionCodes::DECDHL_DoubleHeightLineBottom: + _dispatch->SetLineRendition(LineRendition::DoubleHeightBottom); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDHL); + break; + case EscActionCodes::DECSWL_SingleWidthLine: + _dispatch->SetLineRendition(LineRendition::SingleWidth); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECSWL); + break; + case EscActionCodes::DECDWL_DoubleWidthLine: + _dispatch->SetLineRendition(LineRendition::DoubleWidth); + TermTelemetry::Instance().Log(TermTelemetry::Codes::DECDWL); + break; case EscActionCodes::DECALN_ScreenAlignmentPattern: success = _dispatch->ScreenAlignmentPattern(); TermTelemetry::Instance().Log(TermTelemetry::Codes::DECALN); diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 1ad03b643..ccfb49b91 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -86,6 +86,10 @@ namespace Microsoft::Console::VirtualTerminal LS1R_LockingShift = VTID("~"), LS2R_LockingShift = VTID("}"), LS3R_LockingShift = VTID("|"), + DECDHL_DoubleHeightLineTop = VTID("#3"), + DECDHL_DoubleHeightLineBottom = VTID("#4"), + DECSWL_SingleWidthLine = VTID("#5"), + DECDWL_DoubleWidthLine = VTID("#6"), DECALN_ScreenAlignmentPattern = VTID("#8") }; diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp index d1a6da50c..12a2235d4 100644 --- a/src/terminal/parser/telemetry.cpp +++ b/src/terminal/parser/telemetry.cpp @@ -273,6 +273,9 @@ void TermTelemetry::WriteFinalTraceLog() const TraceLoggingUInt32(_uiTimesUsed[OSCBG], "OscBackgroundColor"), TraceLoggingUInt32(_uiTimesUsed[OSCSCB], "OscSetClipboard"), TraceLoggingUInt32(_uiTimesUsed[REP], "REP"), + TraceLoggingUInt32(_uiTimesUsed[DECSWL], "DECSWL"), + TraceLoggingUInt32(_uiTimesUsed[DECDWL], "DECDWL"), + TraceLoggingUInt32(_uiTimesUsed[DECDHL], "DECDHL"), TraceLoggingUInt32(_uiTimesUsed[DECALN], "DECALN"), TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"), TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"), diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp index f472852a2..ec79bf81c 100644 --- a/src/terminal/parser/telemetry.hpp +++ b/src/terminal/parser/telemetry.hpp @@ -99,6 +99,9 @@ namespace Microsoft::Console::VirtualTerminal REP, OSCFG, OSCBG, + DECSWL, + DECDWL, + DECDHL, DECALN, OSCSCB, XTPUSHSGR, diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 89ce8dc65..3d61d23f3 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -410,7 +410,8 @@ IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ // GH#6402: Get the actual buffer size here, instead of the one // constrained by the virtual bottom. - const auto bufferSize = _pData->GetTextBuffer().GetSize(); + const auto& buffer = _pData->GetTextBuffer(); + const auto bufferSize = buffer.GetSize(); // these viewport vars are converted to the buffer coordinate space const auto viewport = bufferSize.ConvertToOrigin(_pData->GetViewport()); @@ -448,11 +449,14 @@ IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ } else { - const auto textRects = _pData->GetTextBuffer().GetTextRects(startAnchor, endAnchor, _blockRange); + const auto textRects = buffer.GetTextRects(startAnchor, endAnchor, _blockRange, true); for (const auto& rect : textRects) { - til::rectangle r{ rect }; + // Convert the buffer coordinates to an equivalent range of + // screen cells, taking line rendition into account. + const auto lineRendition = buffer.GetLineRendition(rect.Top); + til::rectangle r{ BufferToScreenLine(rect, lineRendition) }; r -= viewportOrigin; _getBoundingRect(r, coords); } @@ -551,7 +555,7 @@ std::wstring UiaTextRangeBase::_getTextValue(std::optional maxLeng auto inclusiveEnd = _end; bufferSize.DecrementInBounds(inclusiveEnd, true); - const auto textRects = buffer.GetTextRects(_start, inclusiveEnd, _blockRange); + const auto textRects = buffer.GetTextRects(_start, inclusiveEnd, _blockRange, true); const auto bufferData = buffer.GetText(true, false, textRects);