From cc9d2ca9e393b64478771b6b7c60c7302ac57d08 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 14 Jan 2020 15:34:43 -0600 Subject: [PATCH] Move reflowing the buffer to `TextBuffer` (#4197) ## Summary of the Pull Request In pursuit of reflowing the terminal buffer on resize, move the reflow algorithm to the TextBuffer. This does _not_ yet add support for reflowing in the Windows Terminal. ## References ## PR Checklist * [ ] There's not really an issue for this yet, I'm just breaking this work up into as many PRs as possible to help the inevitable bisect. * [x] I work here * [x] Ideally, all the existing tests will pass * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments In `SCREEN_INFORMATION::ResizeScreenBuffer`, the screenbuffer needs to create a new buffer, and copy the contents of the old buffer into the new one. I'm moving that "copy contents from the old buffer to the new one" step to it's own helper, as a static function on `TextBuffer`. That way, when the time comes to implement this for the Terminal, the hard part of the code will already be there. ## Validation Steps Performed Ideally, all the tests will still pass. --- src/buffer/out/textBuffer.cpp | 222 ++++++++++++++++++++++++++++++++++ src/buffer/out/textBuffer.hpp | 2 + src/host/screenInfo.cpp | 211 +------------------------------- 3 files changed, 228 insertions(+), 207 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 70973fd925d..99cdbc78e54 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1545,3 +1545,225 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi return {}; } } + +// Function Description: +// - Reflow the contents from the old buffer into the new buffer. The new buffer +// can have different dimensions than the old buffer. If it does, then this +// function will attempt to maintain the logical contents of the old buffer, +// by continuing wrapped lines onto the next line in the new buffer. +// Arguments: +// - oldBuffer - the text buffer to copy the contents FROM +// - newBuffer - the text buffer to copy the contents TO +// Return Value: +// - S_OK if we successfully copied the contents to the new buffer, otherwise an appropriate HRESULT. +HRESULT TextBuffer::Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer) +{ + Cursor& oldCursor = oldBuffer.GetCursor(); + Cursor& newCursor = newBuffer.GetCursor(); + // skip any drawing updates that might occur as we manipulate the new buffer + oldCursor.StartDeferDrawing(); + newCursor.StartDeferDrawing(); + + // We need to save the old cursor position so that we can + // place the new cursor back on the equivalent character in + // the new buffer. + const COORD cOldCursorPos = oldCursor.GetPosition(); + const COORD cOldLastChar = oldBuffer.GetLastNonSpaceCharacter(); + + short const cOldRowsTotal = cOldLastChar.Y + 1; + short const cOldColsTotal = oldBuffer.GetSize().Width(); + + COORD cNewCursorPos = { 0 }; + bool fFoundCursorPos = false; + + HRESULT hr = S_OK; + // Loop through all the rows of the old buffer and reprint them into the new buffer + for (short iOldRow = 0; iOldRow < cOldRowsTotal; iOldRow++) + { + // Fetch the row and its "right" which is the last printable character. + const ROW& row = oldBuffer.GetRowByOffset(iOldRow); + const CharRow& charRow = row.GetCharRow(); + short iRight = gsl::narrow_cast(charRow.MeasureRight()); + + // 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 + // were a bunch trailing of spaces in the row. + // (But the measuring functions for each row Left/Right do + // not count spaces as "displayable" so they're not + // included.) + // As such, adjust the "right" to be the width of the row + // to capture all these spaces + if (charRow.WasWrapForced()) + { + iRight = cOldColsTotal; + + // And a combined special case. + // If we wrapped off the end of the row by adding a + // piece of padding because of a double byte LEADING + // character, then remove one from the "right" to + // leave this padding out of the copy process. + if (charRow.WasDoubleBytePadded()) + { + iRight--; + } + } + + // Loop through every character in the current row (up to + // the "right" boundary, which is one past the final valid + // character) + for (short iOldCol = 0; iOldCol < iRight; iOldCol++) + { + if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y) + { + cNewCursorPos = newCursor.GetPosition(); + fFoundCursorPos = true; + } + + try + { + // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... + const auto glyph = row.GetCharRow().GlyphAt(iOldCol); + const auto dbcsAttr = row.GetCharRow().DbcsAttrAt(iOldCol); + const auto textAttr = row.GetAttrRow().GetAttrByColumn(iOldCol); + + if (!newBuffer.InsertCharacter(glyph, dbcsAttr, textAttr)) + { + hr = E_OUTOFMEMORY; + break; + } + } + CATCH_RETURN(); + } + if (SUCCEEDED(hr)) + { + // If we didn't have a full row to copy, insert a new + // line into the new buffer. + // Only do so if we were not forced to wrap. If we did + // force a word wrap, then the existing line break was + // only because we ran out of space. + if (iRight < cOldColsTotal && !charRow.WasWrapForced()) + { + if (iRight == cOldCursorPos.X && iOldRow == cOldCursorPos.Y) + { + cNewCursorPos = newCursor.GetPosition(); + fFoundCursorPos = true; + } + // Only do this if it's not the final line in the buffer. + // On the final line, we want the cursor to sit + // where it is done printing for the cursor + // adjustment to follow. + if (iOldRow < cOldRowsTotal - 1) + { + hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY; + } + else + { + // If we are on the final line of the buffer, we have one more check. + // We got into this code path because we are at the right most column of a row in the old buffer + // that had a hard return (no wrap was forced). + // However, as we're inserting, the old row might have just barely fit into the new buffer and + // caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below. + // We need to preserve the memory of the hard return at this point by inserting one additional + // hard newline, otherwise we've lost that information. + // We only do this when the cursor has just barely poured over onto the next line so the hard return + // isn't covered by the soft one. + // e.g. + // The old line was: + // |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a. + // The cursor was here ^ + // And the new line will be: + // |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end + // | | + // ^ and the cursor is now there. + // If we leave it like this, we've lost the newline information. + // So we insert one more newline so a continued reflow of this buffer by resizing larger will + // continue to look as the original output intended with the newline data. + // After this fix, it looks like this: + // |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline) + // | | + // ^ and the cursor is now here. + const COORD coordNewCursor = newCursor.GetPosition(); + if (coordNewCursor.X == 0 && coordNewCursor.Y > 0) + { + if (newBuffer.GetRowByOffset(gsl::narrow_cast(coordNewCursor.Y) - 1).GetCharRow().WasWrapForced()) + { + hr = newBuffer.NewlineCursor() ? hr : E_OUTOFMEMORY; + } + } + } + } + } + } + if (SUCCEEDED(hr)) + { + // Finish copying remaining parameters from the old text buffer to the new one + newBuffer.CopyProperties(oldBuffer); + + // If we found where to put the cursor while placing characters into the buffer, + // just put the cursor there. Otherwise we have to advance manually. + if (fFoundCursorPos) + { + newCursor.SetPosition(cNewCursorPos); + } + else + { + // Advance the cursor to the same offset as before + // get the number of newlines and spaces between the old end of text and the old cursor, + // then advance that many newlines and chars + int iNewlines = cOldCursorPos.Y - cOldLastChar.Y; + const int iIncrements = cOldCursorPos.X - cOldLastChar.X; + const COORD cNewLastChar = newBuffer.GetLastNonSpaceCharacter(); + + // If the last row of the new buffer wrapped, there's going to be one less newline needed, + // because the cursor is already on the next line + if (newBuffer.GetRowByOffset(cNewLastChar.Y).GetCharRow().WasWrapForced()) + { + iNewlines = std::max(iNewlines - 1, 0); + } + else + { + // if this buffer didn't wrap, but the old one DID, then the d(columns) of the + // old buffer will be one more than in this buffer, so new need one LESS. + if (oldBuffer.GetRowByOffset(cOldLastChar.Y).GetCharRow().WasWrapForced()) + { + iNewlines = std::max(iNewlines - 1, 0); + } + } + + for (int r = 0; r < iNewlines; r++) + { + if (!newBuffer.NewlineCursor()) + { + hr = E_OUTOFMEMORY; + break; + } + } + if (SUCCEEDED(hr)) + { + for (int c = 0; c < iIncrements - 1; c++) + { + if (!newBuffer.IncrementCursor()) + { + hr = E_OUTOFMEMORY; + break; + } + } + } + } + } + + if (SUCCEEDED(hr)) + { + // Save old cursor size before we delete it + ULONG const ulSize = oldCursor.GetSize(); + + // Set size back to real size as it will be taking over the rendering duties. + newCursor.SetSize(ulSize); + } + + newCursor.EndDeferDrawing(); + oldCursor.EndDeferDrawing(); + + return hr; +} diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index f77f879a82b..4d70bb48303 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -158,6 +158,8 @@ class TextBuffer final const std::wstring_view fontFaceName, const COLORREF backgroundColor); + static HRESULT Reflow(TextBuffer& oldBuffer, TextBuffer& newBuffer); + private: std::deque _storage; Cursor _cursor; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index f6d7e4407af..e5740bdce40 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1425,224 +1425,21 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // Save cursor's relative height versus the viewport SHORT const sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top(); - Cursor& oldCursor = _textBuffer->GetCursor(); - Cursor& newCursor = newTextBuffer->GetCursor(); - // skip any drawing updates that might occur as we manipulate the new buffer - oldCursor.StartDeferDrawing(); - newCursor.StartDeferDrawing(); + HRESULT hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get()); - // We need to save the old cursor position so that we can - // place the new cursor back on the equivalent character in - // the new buffer. - COORD cOldCursorPos = oldCursor.GetPosition(); - COORD cOldLastChar = _textBuffer->GetLastNonSpaceCharacter(); - - short const cOldRowsTotal = cOldLastChar.Y + 1; - short const cOldColsTotal = GetBufferSize().Width(); - - COORD cNewCursorPos = { 0 }; - bool fFoundCursorPos = false; - - NTSTATUS status = STATUS_SUCCESS; - // Loop through all the rows of the old buffer and reprint them into the new buffer - for (short iOldRow = 0; iOldRow < cOldRowsTotal; iOldRow++) - { - // Fetch the row and its "right" which is the last printable character. - const ROW& Row = _textBuffer->GetRowByOffset(iOldRow); - const CharRow& charRow = Row.GetCharRow(); - short iRight = static_cast(charRow.MeasureRight()); - - // 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 - // were a bunch trailing of spaces in the row. - // (But the measuring functions for each row Left/Right do - // not count spaces as "displayable" so they're not - // included.) - // As such, adjust the "right" to be the width of the row - // to capture all these spaces - if (charRow.WasWrapForced()) - { - iRight = cOldColsTotal; - - // And a combined special case. - // If we wrapped off the end of the row by adding a - // piece of padding because of a double byte LEADING - // character, then remove one from the "right" to - // leave this padding out of the copy process. - if (charRow.WasDoubleBytePadded()) - { - iRight--; - } - } - - // Loop through every character in the current row (up to - // the "right" boundary, which is one past the final valid - // character) - for (short iOldCol = 0; iOldCol < iRight; iOldCol++) - { - if (iOldCol == cOldCursorPos.X && iOldRow == cOldCursorPos.Y) - { - cNewCursorPos = newCursor.GetPosition(); - fFoundCursorPos = true; - } - - try - { - // TODO: MSFT: 19446208 - this should just use an iterator and the inserter... - const auto glyph = Row.GetCharRow().GlyphAt(iOldCol); - const auto dbcsAttr = Row.GetCharRow().DbcsAttrAt(iOldCol); - const auto textAttr = Row.GetAttrRow().GetAttrByColumn(iOldCol); - - if (!newTextBuffer->InsertCharacter(glyph, dbcsAttr, textAttr)) - { - status = STATUS_NO_MEMORY; - break; - } - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } - } - if (NT_SUCCESS(status)) - { - // If we didn't have a full row to copy, insert a new - // line into the new buffer. - // Only do so if we were not forced to wrap. If we did - // force a word wrap, then the existing line break was - // only because we ran out of space. - if (iRight < cOldColsTotal && !charRow.WasWrapForced()) - { - if (iRight == cOldCursorPos.X && iOldRow == cOldCursorPos.Y) - { - cNewCursorPos = newCursor.GetPosition(); - fFoundCursorPos = true; - } - // Only do this if it's not the final line in the buffer. - // On the final line, we want the cursor to sit - // where it is done printing for the cursor - // adjustment to follow. - if (iOldRow < cOldRowsTotal - 1) - { - status = newTextBuffer->NewlineCursor() ? status : STATUS_NO_MEMORY; - } - else - { - // If we are on the final line of the buffer, we have one more check. - // We got into this code path because we are at the right most column of a row in the old buffer - // that had a hard return (no wrap was forced). - // However, as we're inserting, the old row might have just barely fit into the new buffer and - // caused a new soft return (wrap was forced) putting the cursor at x=0 on the line just below. - // We need to preserve the memory of the hard return at this point by inserting one additional - // hard newline, otherwise we've lost that information. - // We only do this when the cursor has just barely poured over onto the next line so the hard return - // isn't covered by the soft one. - // e.g. - // The old line was: - // |aaaaaaaaaaaaaaaaaaa | with no wrap which means there was a newline after that final a. - // The cursor was here ^ - // And the new line will be: - // |aaaaaaaaaaaaaaaaaaa| and show a wrap at the end - // | | - // ^ and the cursor is now there. - // If we leave it like this, we've lost the newline information. - // So we insert one more newline so a continued reflow of this buffer by resizing larger will - // continue to look as the original output intended with the newline data. - // After this fix, it looks like this: - // |aaaaaaaaaaaaaaaaaaa| no wrap at the end (preserved hard newline) - // | | - // ^ and the cursor is now here. - const COORD coordNewCursor = newCursor.GetPosition(); - if (coordNewCursor.X == 0 && coordNewCursor.Y > 0) - { - if (newTextBuffer->GetRowByOffset(coordNewCursor.Y - 1).GetCharRow().WasWrapForced()) - { - status = newTextBuffer->NewlineCursor() ? status : STATUS_NO_MEMORY; - } - } - } - } - } - } - if (NT_SUCCESS(status)) - { - // Finish copying remaining parameters from the old text buffer to the new one - newTextBuffer->CopyProperties(*_textBuffer); - - // If we found where to put the cursor while placing characters into the buffer, - // just put the cursor there. Otherwise we have to advance manually. - if (fFoundCursorPos) - { - newCursor.SetPosition(cNewCursorPos); - } - else - { - // Advance the cursor to the same offset as before - // get the number of newlines and spaces between the old end of text and the old cursor, - // then advance that many newlines and chars - int iNewlines = cOldCursorPos.Y - cOldLastChar.Y; - int iIncrements = cOldCursorPos.X - cOldLastChar.X; - const COORD cNewLastChar = newTextBuffer->GetLastNonSpaceCharacter(); - - // If the last row of the new buffer wrapped, there's going to be one less newline needed, - // because the cursor is already on the next line - if (newTextBuffer->GetRowByOffset(cNewLastChar.Y).GetCharRow().WasWrapForced()) - { - iNewlines = std::max(iNewlines - 1, 0); - } - else - { - // if this buffer didn't wrap, but the old one DID, then the d(columns) of the - // old buffer will be one more than in this buffer, so new need one LESS. - if (_textBuffer->GetRowByOffset(cOldLastChar.Y).GetCharRow().WasWrapForced()) - { - iNewlines = std::max(iNewlines - 1, 0); - } - } - - for (int r = 0; r < iNewlines; r++) - { - if (!newTextBuffer->NewlineCursor()) - { - status = STATUS_NO_MEMORY; - break; - } - } - if (NT_SUCCESS(status)) - { - for (int c = 0; c < iIncrements - 1; c++) - { - if (!newTextBuffer->IncrementCursor()) - { - status = STATUS_NO_MEMORY; - break; - } - } - } - } - } - - if (NT_SUCCESS(status)) + if (SUCCEEDED(hr)) { + Cursor& newCursor = newTextBuffer->GetCursor(); // Adjust the viewport so the cursor doesn't wildly fly off up or down. SHORT const sCursorHeightInViewportAfter = newCursor.GetPosition().Y - _viewport.Top(); COORD coordCursorHeightDiff = { 0 }; coordCursorHeightDiff.Y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, true)); - // Save old cursor size before we delete it - ULONG const ulSize = oldCursor.GetSize(); - _textBuffer.swap(newTextBuffer); - - // Set size back to real size as it will be taking over the rendering duties. - newCursor.SetSize(ulSize); - newCursor.EndDeferDrawing(); } - oldCursor.EndDeferDrawing(); - return status; + return NTSTATUS_FROM_HRESULT(hr); } //