diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index a4eaa73140d..c5829dc16f3 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1541,3 +1541,215 @@ std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoi return {}; } } + +HRESULT TextBuffer::ReflowBuffer(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. + COORD cOldCursorPos = oldCursor.GetPosition(); + 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 = 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 (!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(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; + 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..f8b23ac1c24 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 ReflowBuffer(TextBuffer& oldBuffer, TextBuffer& newBuffer); + private: std::deque _storage; Cursor _cursor; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index f6d7e4407af..34e5c522b4c 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::ReflowBuffer(*_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); } //