Skip to content

Commit

Permalink
Move reflowing the buffer to TextBuffer (#4197)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
zadjii-msft authored and msftbot[bot] committed Jan 14, 2020
1 parent 171e906 commit cc9d2ca
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 207 deletions.
222 changes: 222 additions & 0 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<short>(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<size_t>(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;
}
2 changes: 2 additions & 0 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ROW> _storage;
Cursor _cursor;
Expand Down
Loading

0 comments on commit cc9d2ca

Please sign in to comment.