mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-13 09:41:02 +00:00
This is a refactoring of the renderer color calculations to simplify the implementation, and to make it easier to support additional color-altering rendition attributes in the future (e.g. _faint_ and _conceal_). ## References * This is a followup to PRs #3817 and #6809, which introduced additional complexity in the color calculations, and which suggested the need for refactoring. ## Detailed Description of the Pull Request / Additional comments When we added support for `DECSCNM`, that required the foreground and background color lookup methods to be able to return the opposite of what was requested when the reversed mode was set. That made those methods unnecessarily complicated, and I thought we could simplify them considerably just by combining the calculations into a single method that derived both colors at the same time. And since both conhost and Windows Terminal needed to perform the same calculations, it also made sense to move that functionality into the `TextAttribute` class, where it could easily be shared. In general this way of doing things is a bit more efficient. However, it does result in some unnecessary work when only one of the colors is required, as is the case for the gridline painter. So to make that less of an issue, I've reordered the gridline code a bit so it at least avoids looking up the colors when no gridlines are needed. ## Validation Steps Performed Because of the API changes, quite a lot of the unit tests had to be updated. For example instead of verifying colors with two separate calls to `LookupForegroundColor` and `LookupBackgroundColor`, that's now achieved with a single `LookupAttributeColors` call, comparing against a pair of values. The specifics of the tests haven't changed though, and they're all still working as expected. I've also manually confirmed that the various color sequences and rendition attributes are rendering correctly with the new refactoring.
2210 lines
83 KiB
C++
2210 lines
83 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "textBuffer.hpp"
|
|
#include "CharRow.hpp"
|
|
|
|
#include "../types/inc/utils.hpp"
|
|
#include "../types/inc/convert.hpp"
|
|
|
|
#pragma hdrstop
|
|
|
|
using namespace Microsoft::Console;
|
|
using namespace Microsoft::Console::Types;
|
|
|
|
// Routine Description:
|
|
// - Creates a new instance of TextBuffer
|
|
// Arguments:
|
|
// - fontInfo - The font to use for this text buffer as specified in the global font cache
|
|
// - screenBufferSize - The X by Y dimensions of the new screen buffer
|
|
// - fill - Uses the .Attributes property to decide which default color to apply to all text in this buffer
|
|
// - cursorSize - The height of the cursor within this buffer
|
|
// Return Value:
|
|
// - constructed object
|
|
// Note: may throw exception
|
|
TextBuffer::TextBuffer(const COORD screenBufferSize,
|
|
const TextAttribute defaultAttributes,
|
|
const UINT cursorSize,
|
|
Microsoft::Console::Render::IRenderTarget& renderTarget) :
|
|
_firstRow{ 0 },
|
|
_currentAttributes{ defaultAttributes },
|
|
_cursor{ cursorSize, *this },
|
|
_storage{},
|
|
_unicodeStorage{},
|
|
_renderTarget{ renderTarget },
|
|
_size{}
|
|
{
|
|
// initialize ROWs
|
|
for (size_t i = 0; i < static_cast<size_t>(screenBufferSize.Y); ++i)
|
|
{
|
|
_storage.emplace_back(static_cast<SHORT>(i), screenBufferSize.X, _currentAttributes, this);
|
|
}
|
|
|
|
_UpdateSize();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Copies properties from another text buffer into this one.
|
|
// - This is primarily to copy properties that would otherwise not be specified during CreateInstance
|
|
// Arguments:
|
|
// - OtherBuffer - The text buffer to copy properties from
|
|
// Return Value:
|
|
// - <none>
|
|
void TextBuffer::CopyProperties(const TextBuffer& OtherBuffer) noexcept
|
|
{
|
|
GetCursor().CopyProperties(OtherBuffer.GetCursor());
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Gets the number of rows in the buffer
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - Total number of rows in the buffer
|
|
UINT TextBuffer::TotalRowCount() const noexcept
|
|
{
|
|
return gsl::narrow<UINT>(_storage.size());
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to
|
|
// the top row of the screen buffer)
|
|
// Arguments:
|
|
// - Number of rows down from the first row of the buffer.
|
|
// Return Value:
|
|
// - const reference to the requested row. Asserts if out of bounds.
|
|
const ROW& TextBuffer::GetRowByOffset(const size_t index) const
|
|
{
|
|
const size_t totalRows = TotalRowCount();
|
|
|
|
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
|
|
const size_t offsetIndex = (_firstRow + index) % totalRows;
|
|
return _storage.at(offsetIndex);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to
|
|
// the top row of the screen buffer)
|
|
// Arguments:
|
|
// - Number of rows down from the first row of the buffer.
|
|
// Return Value:
|
|
// - reference to the requested row. Asserts if out of bounds.
|
|
ROW& TextBuffer::GetRowByOffset(const size_t index)
|
|
{
|
|
const size_t totalRows = TotalRowCount();
|
|
|
|
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
|
|
const size_t offsetIndex = (_firstRow + index) % totalRows;
|
|
return _storage.at(offsetIndex);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves read-only text iterator at the given buffer location
|
|
// Arguments:
|
|
// - at - X,Y position in buffer for iterator start position
|
|
// Return Value:
|
|
// - Read-only iterator of text data only.
|
|
TextBufferTextIterator TextBuffer::GetTextDataAt(const COORD at) const
|
|
{
|
|
return TextBufferTextIterator(GetCellDataAt(at));
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves read-only cell iterator at the given buffer location
|
|
// Arguments:
|
|
// - at - X,Y position in buffer for iterator start position
|
|
// Return Value:
|
|
// - Read-only iterator of cell data.
|
|
TextBufferCellIterator TextBuffer::GetCellDataAt(const COORD at) const
|
|
{
|
|
return TextBufferCellIterator(*this, at);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves read-only text iterator at the given buffer location
|
|
// but restricted to only the specific line (Y coordinate).
|
|
// Arguments:
|
|
// - at - X,Y position in buffer for iterator start position
|
|
// Return Value:
|
|
// - Read-only iterator of text data only.
|
|
TextBufferTextIterator TextBuffer::GetTextLineDataAt(const COORD at) const
|
|
{
|
|
return TextBufferTextIterator(GetCellLineDataAt(at));
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves read-only cell iterator at the given buffer location
|
|
// but restricted to only the specific line (Y coordinate).
|
|
// Arguments:
|
|
// - at - X,Y position in buffer for iterator start position
|
|
// Return Value:
|
|
// - Read-only iterator of cell data.
|
|
TextBufferCellIterator TextBuffer::GetCellLineDataAt(const COORD at) const
|
|
{
|
|
SMALL_RECT limit;
|
|
limit.Top = at.Y;
|
|
limit.Bottom = at.Y;
|
|
limit.Left = 0;
|
|
limit.Right = GetSize().RightInclusive();
|
|
|
|
return TextBufferCellIterator(*this, at, Viewport::FromInclusive(limit));
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves read-only text iterator at the given buffer location
|
|
// but restricted to operate only inside the given viewport.
|
|
// Arguments:
|
|
// - at - X,Y position in buffer for iterator start position
|
|
// - limit - boundaries for the iterator to operate within
|
|
// Return Value:
|
|
// - Read-only iterator of text data only.
|
|
TextBufferTextIterator TextBuffer::GetTextDataAt(const COORD at, const Viewport limit) const
|
|
{
|
|
return TextBufferTextIterator(GetCellDataAt(at, limit));
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves read-only cell iterator at the given buffer location
|
|
// but restricted to operate only inside the given viewport.
|
|
// Arguments:
|
|
// - at - X,Y position in buffer for iterator start position
|
|
// - limit - boundaries for the iterator to operate within
|
|
// Return Value:
|
|
// - Read-only iterator of cell data.
|
|
TextBufferCellIterator TextBuffer::GetCellDataAt(const COORD at, const Viewport limit) const
|
|
{
|
|
return TextBufferCellIterator(*this, at, limit);
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Corrects and enforces consistent double byte character state (KAttrs line) within a row of the text buffer.
|
|
// - This will take the given double byte information and check that it will be consistent when inserted into the buffer
|
|
// at the current cursor position.
|
|
// - It will correct the buffer (by erasing the character prior to the cursor) if necessary to make a consistent state.
|
|
//Arguments:
|
|
// - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer
|
|
//Return Value:
|
|
// - True if it is valid to insert a character with the given double byte attributes. False otherwise.
|
|
bool TextBuffer::_AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute)
|
|
{
|
|
// To figure out if the sequence is valid, we have to look at the character that comes before the current one
|
|
const COORD coordPrevPosition = _GetPreviousFromCursor();
|
|
ROW& prevRow = GetRowByOffset(coordPrevPosition.Y);
|
|
DbcsAttribute prevDbcsAttr;
|
|
try
|
|
{
|
|
prevDbcsAttr = prevRow.GetCharRow().DbcsAttrAt(coordPrevPosition.X);
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_HR(wil::ResultFromCaughtException());
|
|
return false;
|
|
}
|
|
|
|
bool fValidSequence = true; // Valid until proven otherwise
|
|
bool fCorrectableByErase = false; // Can't be corrected until proven otherwise
|
|
|
|
// Here's the matrix of valid items:
|
|
// N = None (single byte)
|
|
// L = Lead (leading byte of double byte sequence
|
|
// T = Trail (trailing byte of double byte sequence
|
|
// Prev Curr Result
|
|
// N N OK.
|
|
// N L OK.
|
|
// N T Fail, uncorrectable. Trailing byte must have had leading before it.
|
|
// L N Fail, OK with erase. Lead needs trailing pair. Can erase lead to correct.
|
|
// L L Fail, OK with erase. Lead needs trailing pair. Can erase prev lead to correct.
|
|
// L T OK.
|
|
// T N OK.
|
|
// T L OK.
|
|
// T T Fail, uncorrectable. New trailing byte must have had leading before it.
|
|
|
|
// Check for only failing portions of the matrix:
|
|
if (prevDbcsAttr.IsSingle() && dbcsAttribute.IsTrailing())
|
|
{
|
|
// N, T failing case (uncorrectable)
|
|
fValidSequence = false;
|
|
}
|
|
else if (prevDbcsAttr.IsLeading())
|
|
{
|
|
if (dbcsAttribute.IsSingle() || dbcsAttribute.IsLeading())
|
|
{
|
|
// L, N and L, L failing cases (correctable)
|
|
fValidSequence = false;
|
|
fCorrectableByErase = true;
|
|
}
|
|
}
|
|
else if (prevDbcsAttr.IsTrailing() && dbcsAttribute.IsTrailing())
|
|
{
|
|
// T, T failing case (uncorrectable)
|
|
fValidSequence = false;
|
|
}
|
|
|
|
// If it's correctable by erase, erase the previous character
|
|
if (fCorrectableByErase)
|
|
{
|
|
// Erase previous character into an N type.
|
|
try
|
|
{
|
|
prevRow.GetCharRow().ClearCell(coordPrevPosition.X);
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_HR(wil::ResultFromCaughtException());
|
|
return false;
|
|
}
|
|
|
|
// Sequence is now N N or N L, which are both okay. Set sequence back to valid.
|
|
fValidSequence = true;
|
|
}
|
|
|
|
return fValidSequence;
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Call before inserting a character into the buffer.
|
|
// - This will ensure a consistent double byte state (KAttrs line) within the text buffer
|
|
// - It will attempt to correct the buffer if we're inserting an unexpected double byte character type
|
|
// and it will pad out the buffer if we're going to split a double byte sequence across two rows.
|
|
//Arguments:
|
|
// - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer
|
|
//Return Value:
|
|
// - true if we successfully prepared the buffer and moved the cursor
|
|
// - false otherwise (out of memory)
|
|
bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute)
|
|
{
|
|
// Assert the buffer state is ready for this character
|
|
// This function corrects most errors. If this is false, we had an uncorrectable one.
|
|
FAIL_FAST_IF(!(_AssertValidDoubleByteSequence(dbcsAttribute))); // Shouldn't be uncorrectable sequences unless something is very wrong.
|
|
|
|
bool fSuccess = true;
|
|
// Now compensate if we don't have enough space for the upcoming double byte sequence
|
|
// We only need to compensate for leading bytes
|
|
if (dbcsAttribute.IsLeading())
|
|
{
|
|
short const sBufferWidth = GetSize().Width();
|
|
|
|
// 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)
|
|
{
|
|
// set that we're wrapping for double byte reasons
|
|
CharRow& charRow = GetRowByOffset(GetCursor().GetPosition().Y).GetCharRow();
|
|
charRow.SetDoubleBytePadded(true);
|
|
|
|
// then move the cursor forward and onto the next row
|
|
fSuccess = IncrementCursor();
|
|
}
|
|
}
|
|
return fSuccess;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Writes cells to the output buffer. Writes at the cursor.
|
|
// Arguments:
|
|
// - givenIt - Iterator representing output cell data to write
|
|
// Return Value:
|
|
// - The final position of the iterator
|
|
OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt)
|
|
{
|
|
const auto& cursor = GetCursor();
|
|
const auto target = cursor.GetPosition();
|
|
|
|
const auto finalIt = Write(givenIt, target);
|
|
|
|
return finalIt;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Writes cells to the output buffer.
|
|
// Arguments:
|
|
// - givenIt - Iterator representing output cell data to write
|
|
// - target - the row/column to start writing the text to
|
|
// - wrap - change the wrap flag if we hit the end of the row while writing and there's still more data
|
|
// Return Value:
|
|
// - The final position of the iterator
|
|
OutputCellIterator TextBuffer::Write(const OutputCellIterator givenIt,
|
|
const COORD target,
|
|
const std::optional<bool> wrap)
|
|
{
|
|
// Make mutable copy so we can walk.
|
|
auto it = givenIt;
|
|
|
|
// Make mutable target so we can walk down lines.
|
|
auto lineTarget = target;
|
|
|
|
// Get size of the text buffer so we can stay in bounds.
|
|
const auto size = GetSize();
|
|
|
|
// While there's still data in the iterator and we're still targeting in bounds...
|
|
while (it && size.IsInBounds(lineTarget))
|
|
{
|
|
// Attempt to write as much data as possible onto this line.
|
|
// NOTE: if wrap = true/false, we want to set the line's wrap to true/false (respectively) if we reach the end of the line
|
|
it = WriteLine(it, lineTarget, wrap);
|
|
|
|
// Move to the next line down.
|
|
lineTarget.X = 0;
|
|
++lineTarget.Y;
|
|
}
|
|
|
|
return it;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Writes one line of text to the output buffer.
|
|
// Arguments:
|
|
// - givenIt - The iterator that will dereference into cell data to insert
|
|
// - target - Coordinate targeted within output buffer
|
|
// - wrap - change the wrap flag if we hit the end of the row while writing and there's still more data in the iterator.
|
|
// - limitRight - Optionally restrict the right boundary for writing (e.g. stop writing earlier than the end of line)
|
|
// Return Value:
|
|
// - The iterator, but advanced to where we stopped writing. Use to find input consumed length or cells written length.
|
|
OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt,
|
|
const COORD target,
|
|
const std::optional<bool> wrap,
|
|
std::optional<size_t> limitRight)
|
|
{
|
|
// If we're not in bounds, exit early.
|
|
if (!GetSize().IsInBounds(target))
|
|
{
|
|
return givenIt;
|
|
}
|
|
|
|
// Get the row and write the cells
|
|
ROW& row = GetRowByOffset(target.Y);
|
|
const auto newIt = row.WriteCells(givenIt, target.X, wrap, limitRight);
|
|
|
|
// Take the cell distance written and notify that it needs to be repainted.
|
|
const auto written = newIt.GetCellDistance(givenIt);
|
|
const Viewport paint = Viewport::FromDimensions(target, { gsl::narrow<SHORT>(written), 1 });
|
|
_NotifyPaint(paint);
|
|
|
|
return newIt;
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Inserts one codepoint into the buffer at the current cursor position and advances the cursor as appropriate.
|
|
//Arguments:
|
|
// - chars - The codepoint to insert
|
|
// - dbcsAttribute - Double byte information associated with the codepoint
|
|
// - bAttr - Color data associated with the character
|
|
//Return Value:
|
|
// - true if we successfully inserted the character
|
|
// - false otherwise (out of memory)
|
|
bool TextBuffer::InsertCharacter(const std::wstring_view chars,
|
|
const DbcsAttribute dbcsAttribute,
|
|
const TextAttribute attr)
|
|
{
|
|
// Ensure consistent buffer state for double byte characters based on the character type we're about to insert
|
|
bool fSuccess = _PrepareForDoubleByteSequence(dbcsAttribute);
|
|
|
|
if (fSuccess)
|
|
{
|
|
// Get the current cursor position
|
|
short const iRow = GetCursor().GetPosition().Y; // row stored as logical position, not array position
|
|
short const iCol = GetCursor().GetPosition().X; // column logical and array positions are equal.
|
|
|
|
// Get the row associated with the given logical position
|
|
ROW& Row = GetRowByOffset(iRow);
|
|
|
|
// Store character and double byte data
|
|
CharRow& charRow = Row.GetCharRow();
|
|
short const cBufferWidth = GetSize().Width();
|
|
|
|
try
|
|
{
|
|
charRow.GlyphAt(iCol) = chars;
|
|
charRow.DbcsAttrAt(iCol) = dbcsAttribute;
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_HR(wil::ResultFromCaughtException());
|
|
return false;
|
|
}
|
|
|
|
// Store color data
|
|
fSuccess = Row.GetAttrRow().SetAttrToEnd(iCol, attr);
|
|
if (fSuccess)
|
|
{
|
|
// Advance the cursor
|
|
fSuccess = IncrementCursor();
|
|
}
|
|
}
|
|
return fSuccess;
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Inserts one ucs2 codepoint into the buffer at the current cursor position and advances the cursor as appropriate.
|
|
//Arguments:
|
|
// - wch - The codepoint to insert
|
|
// - dbcsAttribute - Double byte information associated with the codepoint
|
|
// - bAttr - Color data associated with the character
|
|
//Return Value:
|
|
// - true if we successfully inserted the character
|
|
// - false otherwise (out of memory)
|
|
bool TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr)
|
|
{
|
|
return InsertCharacter({ &wch, 1 }, dbcsAttribute, attr);
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Finds the current row in the buffer (as indicated by the cursor position)
|
|
// and specifies that we have forced a line wrap on that row
|
|
//Arguments:
|
|
// - <none> - Always sets to wrap
|
|
//Return Value:
|
|
// - <none>
|
|
void TextBuffer::_SetWrapOnCurrentRow()
|
|
{
|
|
_AdjustWrapOnCurrentRow(true);
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Finds the current row in the buffer (as indicated by the cursor position)
|
|
// and specifies whether or not it should have a line wrap flag.
|
|
//Arguments:
|
|
// - fSet - True if this row has a wrap. False otherwise.
|
|
//Return Value:
|
|
// - <none>
|
|
void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet)
|
|
{
|
|
// The vertical position of the cursor represents the current row we're manipulating.
|
|
const UINT uiCurrentRowOffset = GetCursor().GetPosition().Y;
|
|
|
|
// Set the wrap status as appropriate
|
|
GetRowByOffset(uiCurrentRowOffset).GetCharRow().SetWrapForced(fSet);
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Increments the cursor one position in the buffer as if text is being typed into the buffer.
|
|
// - NOTE: Will introduce a wrap marker if we run off the end of the current row
|
|
//Arguments:
|
|
// - <none>
|
|
//Return Value:
|
|
// - true if we successfully moved the cursor.
|
|
// - false otherwise (out of memory)
|
|
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();
|
|
|
|
// Move the cursor one position to the right
|
|
GetCursor().IncrementXPosition(1);
|
|
|
|
bool fSuccess = true;
|
|
// If we've passed the final valid column...
|
|
if (GetCursor().GetPosition().X > iFinalColumnIndex)
|
|
{
|
|
// Then mark that we've been forced to wrap
|
|
_SetWrapOnCurrentRow();
|
|
|
|
// Then move the cursor to a new line
|
|
fSuccess = NewlineCursor();
|
|
}
|
|
return fSuccess;
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Increments the cursor one line down in the buffer and to the beginning of the line
|
|
//Arguments:
|
|
// - <none>
|
|
//Return Value:
|
|
// - true if we successfully moved the cursor.
|
|
bool TextBuffer::NewlineCursor()
|
|
{
|
|
bool fSuccess = false;
|
|
short const iFinalRowIndex = GetSize().BottomInclusive();
|
|
|
|
// Reset the cursor position to 0 and move down one line
|
|
GetCursor().SetXPosition(0);
|
|
GetCursor().IncrementYPosition(1);
|
|
|
|
// If we've passed the final valid row...
|
|
if (GetCursor().GetPosition().Y > iFinalRowIndex)
|
|
{
|
|
// Stay on the final logical/offset row of the buffer.
|
|
GetCursor().SetYPosition(iFinalRowIndex);
|
|
|
|
// Instead increment the circular buffer to move us into the "oldest" row of the backing buffer
|
|
fSuccess = IncrementCircularBuffer();
|
|
}
|
|
else
|
|
{
|
|
fSuccess = true;
|
|
}
|
|
return fSuccess;
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable.
|
|
//Arguments:
|
|
// - inVtMode - set to true in VT mode, so standard erase attributes are used for the new row.
|
|
//Return Value:
|
|
// - true if we successfully incremented the buffer.
|
|
bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
|
|
{
|
|
// FirstRow is at any given point in time the array index in the circular buffer that corresponds
|
|
// to the logical position 0 in the window (cursor coordinates and all other coordinates).
|
|
_renderTarget.TriggerCircling();
|
|
|
|
// First, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed.
|
|
auto fillAttributes = _currentAttributes;
|
|
if (inVtMode)
|
|
{
|
|
// The VT standard requires that the new row is initialized with
|
|
// the current background color, but with no meta attributes set.
|
|
fillAttributes.SetStandardErase();
|
|
}
|
|
const bool fSuccess = _storage.at(_firstRow).Reset(fillAttributes);
|
|
if (fSuccess)
|
|
{
|
|
// Now proceed to increment.
|
|
// Incrementing it will cause the next line down to become the new "top" of the window (the new "0" in logical coordinates)
|
|
_firstRow++;
|
|
|
|
// If we pass up the height of the buffer, loop back to 0.
|
|
if (_firstRow >= GetSize().Height())
|
|
{
|
|
_firstRow = 0;
|
|
}
|
|
}
|
|
return fSuccess;
|
|
}
|
|
|
|
//Routine Description:
|
|
// - Retrieves the position of the last non-space character in the given
|
|
// viewport
|
|
// - By default, we search the entire buffer to find the last non-space
|
|
// character.
|
|
// - If we know the last character is within the given viewport (so we don't
|
|
// need to check the entire buffer), we can provide a value in viewOptional
|
|
// that we'll use to search for the last character in.
|
|
//Arguments:
|
|
// - The viewport
|
|
//Return value:
|
|
// - Coordinate position (relative to the text buffer)
|
|
COORD TextBuffer::GetLastNonSpaceCharacter(std::optional<const Microsoft::Console::Types::Viewport> viewOptional) const
|
|
{
|
|
const auto viewport = viewOptional.has_value() ? viewOptional.value() : GetSize();
|
|
|
|
COORD coordEndOfText = { 0 };
|
|
// Search the given viewport by starting at the bottom.
|
|
coordEndOfText.Y = viewport.BottomInclusive();
|
|
|
|
const auto& currRow = GetRowByOffset(coordEndOfText.Y);
|
|
// The X position of the end of the valid text is the Right draw boundary (which is one beyond the final valid character)
|
|
coordEndOfText.X = gsl::narrow<short>(currRow.GetCharRow().MeasureRight()) - 1;
|
|
|
|
// If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text.
|
|
const auto viewportTop = viewport.Top();
|
|
bool fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > viewportTop); // this row is empty, and we're not at the top
|
|
while (fDoBackUp)
|
|
{
|
|
coordEndOfText.Y--;
|
|
const auto& backupRow = GetRowByOffset(coordEndOfText.Y);
|
|
// We need to back up to the previous row if this line is empty, AND there are more rows
|
|
|
|
coordEndOfText.X = gsl::narrow<short>(backupRow.GetCharRow().MeasureRight()) - 1;
|
|
fDoBackUp = (coordEndOfText.X < 0 && coordEndOfText.Y > viewportTop);
|
|
}
|
|
|
|
// don't allow negative results
|
|
coordEndOfText.Y = std::max(coordEndOfText.Y, 0i16);
|
|
coordEndOfText.X = std::max(coordEndOfText.X, 0i16);
|
|
|
|
return coordEndOfText;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves the position of the previous character relative to the current cursor position
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - Coordinate position in screen coordinates of the character just before the cursor.
|
|
// - NOTE: Will return 0,0 if already in the top left corner
|
|
COORD TextBuffer::_GetPreviousFromCursor() const noexcept
|
|
{
|
|
COORD coordPosition = GetCursor().GetPosition();
|
|
|
|
// If we're not at the left edge, simply move the cursor to the left by one
|
|
if (coordPosition.X > 0)
|
|
{
|
|
coordPosition.X--;
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, only if we're not on the top row (e.g. we don't move anywhere in the top left corner. there is no previous)
|
|
if (coordPosition.Y > 0)
|
|
{
|
|
// move the cursor to the right edge
|
|
coordPosition.X = GetSize().RightInclusive();
|
|
|
|
// and up one line
|
|
coordPosition.Y--;
|
|
}
|
|
}
|
|
|
|
return coordPosition;
|
|
}
|
|
|
|
const SHORT TextBuffer::GetFirstRowIndex() const noexcept
|
|
{
|
|
return _firstRow;
|
|
}
|
|
|
|
const Viewport TextBuffer::GetSize() const noexcept
|
|
{
|
|
return _size;
|
|
}
|
|
|
|
void TextBuffer::_UpdateSize()
|
|
{
|
|
_size = Viewport::FromDimensions({ 0, 0 }, { gsl::narrow<SHORT>(_storage.at(0).size()), gsl::narrow<SHORT>(_storage.size()) });
|
|
}
|
|
|
|
void TextBuffer::_SetFirstRowIndex(const SHORT FirstRowIndex) noexcept
|
|
{
|
|
_firstRow = FirstRowIndex;
|
|
}
|
|
|
|
void TextBuffer::ScrollRows(const SHORT firstRow, const SHORT size, const SHORT delta)
|
|
{
|
|
// If we don't have to move anything, leave early.
|
|
if (delta == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// OK. We're about to play games by moving rows around within the deque to
|
|
// scroll a massive region in a faster way than copying things.
|
|
// To make this easier, first correct the circular buffer to have the first row be 0 again.
|
|
if (_firstRow != 0)
|
|
{
|
|
// Rotate the buffer to put the first row at the front.
|
|
std::rotate(_storage.begin(), _storage.begin() + _firstRow, _storage.end());
|
|
|
|
// The first row is now at the top.
|
|
_firstRow = 0;
|
|
}
|
|
|
|
// Rotate just the subsection specified
|
|
if (delta < 0)
|
|
{
|
|
// The layout is like this:
|
|
// delta is -2, size is 3, firstRow is 5
|
|
// We want 3 rows from 5 (5, 6, and 7) to move up 2 spots.
|
|
// --- (storage) ----
|
|
// | 0 begin
|
|
// | 1
|
|
// | 2
|
|
// | 3 A. begin + firstRow + delta (because delta is negative)
|
|
// | 4
|
|
// | 5 B. begin + firstRow
|
|
// | 6
|
|
// | 7
|
|
// | 8 C. begin + firstRow + size
|
|
// | 9
|
|
// | 10
|
|
// | 11
|
|
// - end
|
|
// We want B to slide up to A (the negative delta) and everything from [B,C) to slide up with it.
|
|
// So the final layout will be
|
|
// --- (storage) ----
|
|
// | 0 begin
|
|
// | 1
|
|
// | 2
|
|
// | 5
|
|
// | 6
|
|
// | 7
|
|
// | 3
|
|
// | 4
|
|
// | 8
|
|
// | 9
|
|
// | 10
|
|
// | 11
|
|
// - end
|
|
std::rotate(_storage.begin() + firstRow + delta, _storage.begin() + firstRow, _storage.begin() + firstRow + size);
|
|
}
|
|
else
|
|
{
|
|
// The layout is like this:
|
|
// delta is 2, size is 3, firstRow is 5
|
|
// We want 3 rows from 5 (5, 6, and 7) to move down 2 spots.
|
|
// --- (storage) ----
|
|
// | 0 begin
|
|
// | 1
|
|
// | 2
|
|
// | 3
|
|
// | 4
|
|
// | 5 A. begin + firstRow
|
|
// | 6
|
|
// | 7
|
|
// | 8 B. begin + firstRow + size
|
|
// | 9
|
|
// | 10 C. begin + firstRow + size + delta
|
|
// | 11
|
|
// - end
|
|
// We want B-1 to slide down to C-1 (the positive delta) and everything from [A, B) to slide down with it.
|
|
// So the final layout will be
|
|
// --- (storage) ----
|
|
// | 0 begin
|
|
// | 1
|
|
// | 2
|
|
// | 3
|
|
// | 4
|
|
// | 8
|
|
// | 9
|
|
// | 5
|
|
// | 6
|
|
// | 7
|
|
// | 10
|
|
// | 11
|
|
// - end
|
|
std::rotate(_storage.begin() + firstRow, _storage.begin() + firstRow + size, _storage.begin() + firstRow + size + delta);
|
|
}
|
|
|
|
// Renumber the IDs now that we've rearranged where the rows sit within the buffer.
|
|
// Refreshing should also delegate to the UnicodeStorage to re-key all the stored unicode sequences (where applicable).
|
|
_RefreshRowIDs(std::nullopt);
|
|
}
|
|
|
|
Cursor& TextBuffer::GetCursor() noexcept
|
|
{
|
|
return _cursor;
|
|
}
|
|
|
|
const Cursor& TextBuffer::GetCursor() const noexcept
|
|
{
|
|
return _cursor;
|
|
}
|
|
|
|
[[nodiscard]] TextAttribute TextBuffer::GetCurrentAttributes() const noexcept
|
|
{
|
|
return _currentAttributes;
|
|
}
|
|
|
|
void TextBuffer::SetCurrentAttributes(const TextAttribute currentAttributes) noexcept
|
|
{
|
|
_currentAttributes = currentAttributes;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Resets the text contents of this buffer with the default character
|
|
// and the default current color attributes
|
|
void TextBuffer::Reset()
|
|
{
|
|
const auto attr = GetCurrentAttributes();
|
|
|
|
for (auto& row : _storage)
|
|
{
|
|
row.GetCharRow().Reset();
|
|
row.GetAttrRow().Reset(attr);
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - This is the legacy screen resize with minimal changes
|
|
// Arguments:
|
|
// - newSize - new size of screen.
|
|
// Return Value:
|
|
// - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed.
|
|
[[nodiscard]] NTSTATUS TextBuffer::ResizeTraditional(const COORD newSize) noexcept
|
|
{
|
|
RETURN_HR_IF(E_INVALIDARG, newSize.X < 0 || newSize.Y < 0);
|
|
|
|
try
|
|
{
|
|
const auto currentSize = GetSize().Dimensions();
|
|
const auto attributes = GetCurrentAttributes();
|
|
|
|
SHORT TopRow = 0; // new top row of the screen buffer
|
|
if (newSize.Y <= GetCursor().GetPosition().Y)
|
|
{
|
|
TopRow = GetCursor().GetPosition().Y - newSize.Y + 1;
|
|
}
|
|
const SHORT TopRowIndex = (GetFirstRowIndex() + TopRow) % currentSize.Y;
|
|
|
|
// rotate rows until the top row is at index 0
|
|
const ROW& newTopRow = _storage.at(TopRowIndex);
|
|
while (&newTopRow != &_storage.front())
|
|
{
|
|
_storage.push_back(std::move(_storage.front()));
|
|
_storage.pop_front();
|
|
}
|
|
|
|
_SetFirstRowIndex(0);
|
|
|
|
// realloc in the Y direction
|
|
// remove rows if we're shrinking
|
|
while (_storage.size() > static_cast<size_t>(newSize.Y))
|
|
{
|
|
_storage.pop_back();
|
|
}
|
|
// add rows if we're growing
|
|
while (_storage.size() < static_cast<size_t>(newSize.Y))
|
|
{
|
|
_storage.emplace_back(static_cast<short>(_storage.size()), newSize.X, attributes, this);
|
|
}
|
|
|
|
// Now that we've tampered with the row placement, refresh all the row IDs.
|
|
// Also take advantage of the row ID refresh loop to resize the rows in the X dimension
|
|
// and cleanup the UnicodeStorage characters that might fall outside the resized buffer.
|
|
_RefreshRowIDs(newSize.X);
|
|
|
|
// Update the cached size value
|
|
_UpdateSize();
|
|
}
|
|
CATCH_RETURN();
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
const UnicodeStorage& TextBuffer::GetUnicodeStorage() const noexcept
|
|
{
|
|
return _unicodeStorage;
|
|
}
|
|
|
|
UnicodeStorage& TextBuffer::GetUnicodeStorage() noexcept
|
|
{
|
|
return _unicodeStorage;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Method to help refresh all the Row IDs after manipulating the row
|
|
// by shuffling pointers around.
|
|
// - This will also update parent pointers that are stored in depth within the buffer
|
|
// (e.g. it will update CharRow parents pointing at Rows that might have been moved around)
|
|
// - Optionally takes a new row width if we're resizing to perform a resize operation and cleanup
|
|
// any high unicode (UnicodeStorage) runs while we're already looping through the rows.
|
|
// Arguments:
|
|
// - newRowWidth - Optional new value for the row width.
|
|
void TextBuffer::_RefreshRowIDs(std::optional<SHORT> newRowWidth)
|
|
{
|
|
std::unordered_map<SHORT, SHORT> rowMap;
|
|
SHORT i = 0;
|
|
for (auto& it : _storage)
|
|
{
|
|
// Build a map so we can update Unicode Storage
|
|
rowMap.emplace(it.GetId(), i);
|
|
|
|
// Update the IDs
|
|
it.SetId(i++);
|
|
|
|
// Also update the char row parent pointers as they can get shuffled up in the rotates.
|
|
it.GetCharRow().UpdateParent(&it);
|
|
|
|
// Resize the rows in the X dimension if we have a new width
|
|
if (newRowWidth.has_value())
|
|
{
|
|
// Realloc in the X direction
|
|
THROW_IF_FAILED(it.Resize(newRowWidth.value()));
|
|
}
|
|
}
|
|
|
|
// Give the new mapping to Unicode Storage
|
|
_unicodeStorage.Remap(rowMap, newRowWidth);
|
|
}
|
|
|
|
void TextBuffer::_NotifyPaint(const Viewport& viewport) const
|
|
{
|
|
_renderTarget.TriggerRedraw(viewport);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves the first row from the underlying buffer.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - reference to the first row.
|
|
ROW& TextBuffer::_GetFirstRow()
|
|
{
|
|
return GetRowByOffset(0);
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves the row that comes before the given row.
|
|
// - Does not wrap around the screen buffer.
|
|
// Arguments:
|
|
// - The current row.
|
|
// Return Value:
|
|
// - reference to the previous row
|
|
// Note:
|
|
// - will throw exception if called with the first row of the text buffer
|
|
ROW& TextBuffer::_GetPrevRowNoWrap(const ROW& Row)
|
|
{
|
|
int prevRowIndex = Row.GetId() - 1;
|
|
if (prevRowIndex < 0)
|
|
{
|
|
prevRowIndex = TotalRowCount() - 1;
|
|
}
|
|
|
|
THROW_HR_IF(E_FAIL, Row.GetId() == _firstRow);
|
|
return _storage.at(prevRowIndex);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Retrieves this buffer's current render target.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - This buffer's current render target.
|
|
Microsoft::Console::Render::IRenderTarget& TextBuffer::GetRenderTarget() noexcept
|
|
{
|
|
return _renderTarget;
|
|
}
|
|
|
|
// Method Description:
|
|
// - get delimiter class for buffer cell position
|
|
// - used for double click selection and uia word navigation
|
|
// Arguments:
|
|
// - pos: the buffer cell under observation
|
|
// - wordDelimiters: the delimiters defined as a part of the DelimiterClass::DelimiterChar
|
|
// Return Value:
|
|
// - the delimiter class for the given char
|
|
const DelimiterClass TextBuffer::_GetDelimiterClassAt(const COORD pos, const std::wstring_view wordDelimiters) const
|
|
{
|
|
return GetRowByOffset(pos.Y).GetCharRow().DelimiterClassAt(pos.X, wordDelimiters);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Get the COORD for the beginning of the word you are on
|
|
// Arguments:
|
|
// - target - a COORD on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// - accessibilityMode - when enabled, we continue expanding left until we are at the beginning of a readable word.
|
|
// Otherwise, expand left until a character of a new delimiter class is found
|
|
// (or a row boundary is encountered)
|
|
// Return Value:
|
|
// - The COORD for the first character on the "word" (inclusive)
|
|
const COORD TextBuffer::GetWordStart(const COORD target, const std::wstring_view wordDelimiters, bool accessibilityMode) const
|
|
{
|
|
// Consider a buffer with this text in it:
|
|
// " word other "
|
|
// In selection (accessibilityMode = false),
|
|
// a "word" is defined as the range between two delimiters
|
|
// so the words in the example include [" ", "word", " ", "other", " "]
|
|
// In accessibility (accessibilityMode = true),
|
|
// a "word" includes the delimiters after a range of readable characters
|
|
// so the words in the example include ["word ", "other "]
|
|
// NOTE: the start anchor (this one) is inclusive, whereas the end anchor (GetWordEnd) is exclusive
|
|
|
|
// can't expand left
|
|
if (target.X == GetSize().Left())
|
|
{
|
|
return target;
|
|
}
|
|
|
|
if (accessibilityMode)
|
|
{
|
|
return _GetWordStartForAccessibility(target, wordDelimiters);
|
|
}
|
|
else
|
|
{
|
|
return _GetWordStartForSelection(target, wordDelimiters);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for GetWordStart(). Get the COORD for the beginning of the word (accessibility definition) you are on
|
|
// Arguments:
|
|
// - target - a COORD on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// Return Value:
|
|
// - The COORD for the first character on the current/previous READABLE "word" (inclusive)
|
|
const COORD TextBuffer::_GetWordStartForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const
|
|
{
|
|
COORD result = target;
|
|
const auto bufferSize = GetSize();
|
|
bool stayAtOrigin = false;
|
|
|
|
// ignore left boundary. Continue until readable text found
|
|
while (_GetDelimiterClassAt(result, wordDelimiters) != DelimiterClass::RegularChar)
|
|
{
|
|
if (!bufferSize.DecrementInBounds(result))
|
|
{
|
|
// first char in buffer is a DelimiterChar or ControlChar
|
|
// we can't move any further back
|
|
stayAtOrigin = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// make sure we expand to the left boundary or the beginning of the word
|
|
while (_GetDelimiterClassAt(result, wordDelimiters) == DelimiterClass::RegularChar)
|
|
{
|
|
if (!bufferSize.DecrementInBounds(result))
|
|
{
|
|
// first char in buffer is a RegularChar
|
|
// we can't move any further back
|
|
break;
|
|
}
|
|
}
|
|
|
|
// move off of delimiter and onto word start
|
|
if (!stayAtOrigin && _GetDelimiterClassAt(result, wordDelimiters) != DelimiterClass::RegularChar)
|
|
{
|
|
bufferSize.IncrementInBounds(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for GetWordStart(). Get the COORD for the beginning of the word (selection definition) you are on
|
|
// Arguments:
|
|
// - target - a COORD on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// Return Value:
|
|
// - The COORD for the first character on the current word or delimiter run (stopped by the left margin)
|
|
const COORD TextBuffer::_GetWordStartForSelection(const COORD target, const std::wstring_view wordDelimiters) const
|
|
{
|
|
COORD result = target;
|
|
const auto bufferSize = GetSize();
|
|
|
|
const auto initialDelimiter = _GetDelimiterClassAt(result, wordDelimiters);
|
|
|
|
// expand left until we hit the left boundary or a different delimiter class
|
|
while (result.X > bufferSize.Left() && (_GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter))
|
|
{
|
|
bufferSize.DecrementInBounds(result);
|
|
}
|
|
|
|
if (_GetDelimiterClassAt(result, wordDelimiters) != initialDelimiter)
|
|
{
|
|
// move off of delimiter
|
|
bufferSize.IncrementInBounds(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Get the COORD for the beginning of the NEXT word
|
|
// Arguments:
|
|
// - target - a COORD on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// - accessibilityMode - when enabled, we continue expanding right until we are at the beginning of the next READABLE word
|
|
// Otherwise, expand right until a character of a new delimiter class is found
|
|
// (or a row boundary is encountered)
|
|
// Return Value:
|
|
// - The COORD for the last character on the "word" (inclusive)
|
|
const COORD TextBuffer::GetWordEnd(const COORD target, const std::wstring_view wordDelimiters, bool accessibilityMode) const
|
|
{
|
|
// Consider a buffer with this text in it:
|
|
// " word other "
|
|
// In selection (accessibilityMode = false),
|
|
// a "word" is defined as the range between two delimiters
|
|
// so the words in the example include [" ", "word", " ", "other", " "]
|
|
// In accessibility (accessibilityMode = true),
|
|
// a "word" includes the delimiters after a range of readable characters
|
|
// so the words in the example include ["word ", "other "]
|
|
// NOTE: the end anchor (this one) is exclusive, whereas the start anchor (GetWordStart) is inclusive
|
|
|
|
if (accessibilityMode)
|
|
{
|
|
return _GetWordEndForAccessibility(target, wordDelimiters);
|
|
}
|
|
else
|
|
{
|
|
return _GetWordEndForSelection(target, wordDelimiters);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for GetWordEnd(). Get the COORD for the beginning of the next READABLE word
|
|
// Arguments:
|
|
// - target - a COORD on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// Return Value:
|
|
// - The COORD for the first character of the next readable "word". If no next word, return one past the end of the buffer
|
|
const COORD TextBuffer::_GetWordEndForAccessibility(const COORD target, const std::wstring_view wordDelimiters) const
|
|
{
|
|
const auto bufferSize = GetSize();
|
|
COORD result = target;
|
|
|
|
// ignore right boundary. Continue through readable text found
|
|
while (_GetDelimiterClassAt(result, wordDelimiters) == DelimiterClass::RegularChar)
|
|
{
|
|
if (!bufferSize.IncrementInBounds(result, true))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// make sure we expand to the beginning of the NEXT word
|
|
while (_GetDelimiterClassAt(result, wordDelimiters) != DelimiterClass::RegularChar)
|
|
{
|
|
if (!bufferSize.IncrementInBounds(result, true))
|
|
{
|
|
// we are at the EndInclusive COORD
|
|
// this signifies that we must include the last char in the buffer
|
|
// but the position of the COORD points to nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Helper method for GetWordEnd(). Get the COORD for the beginning of the NEXT word
|
|
// Arguments:
|
|
// - target - a COORD on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// Return Value:
|
|
// - The COORD for the last character of the current word or delimiter run (stopped by right margin)
|
|
const COORD TextBuffer::_GetWordEndForSelection(const COORD target, const std::wstring_view wordDelimiters) const
|
|
{
|
|
const auto bufferSize = GetSize();
|
|
|
|
// can't expand right
|
|
if (target.X == bufferSize.RightInclusive())
|
|
{
|
|
return target;
|
|
}
|
|
|
|
COORD result = target;
|
|
const auto initialDelimiter = _GetDelimiterClassAt(result, wordDelimiters);
|
|
|
|
// expand right until we hit the right boundary or a different delimiter class
|
|
while (result.X < bufferSize.RightInclusive() && (_GetDelimiterClassAt(result, wordDelimiters) == initialDelimiter))
|
|
{
|
|
bufferSize.IncrementInBounds(result);
|
|
}
|
|
|
|
if (_GetDelimiterClassAt(result, wordDelimiters) != initialDelimiter)
|
|
{
|
|
// move off of delimiter
|
|
bufferSize.DecrementInBounds(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update pos to be the position of the first character of the next word. This is used for accessibility
|
|
// Arguments:
|
|
// - pos - a COORD on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// - lastCharPos - the position of the last nonspace character in the text buffer (to improve performance)
|
|
// Return Value:
|
|
// - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary)
|
|
// - pos - The COORD for the first character on the "word" (inclusive)
|
|
bool TextBuffer::MoveToNextWord(COORD& pos, const std::wstring_view wordDelimiters, COORD lastCharPos) const
|
|
{
|
|
auto copy = pos;
|
|
const auto bufferSize = GetSize();
|
|
|
|
// started on a word, continue until the end of the word
|
|
while (_GetDelimiterClassAt(copy, wordDelimiters) == DelimiterClass::RegularChar)
|
|
{
|
|
if (!bufferSize.IncrementInBounds(copy))
|
|
{
|
|
// last char in buffer is a RegularChar
|
|
// thus there is no next word
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// we are already on/past the last RegularChar
|
|
if (bufferSize.CompareInBounds(copy, lastCharPos) >= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// on whitespace, continue until the beginning of the next word
|
|
while (_GetDelimiterClassAt(copy, wordDelimiters) != DelimiterClass::RegularChar)
|
|
{
|
|
if (!bufferSize.IncrementInBounds(copy))
|
|
{
|
|
// last char in buffer is a DelimiterChar or ControlChar
|
|
// there is no next word
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// successful move, copy result out
|
|
pos = copy;
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update pos to be the position of the first character of the previous word. This is used for accessibility
|
|
// Arguments:
|
|
// - pos - a COORD on the word you are currently on
|
|
// - wordDelimiters - what characters are we considering for the separation of words
|
|
// Return Value:
|
|
// - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary)
|
|
// - pos - The COORD for the first character on the "word" (inclusive)
|
|
bool TextBuffer::MoveToPreviousWord(COORD& pos, std::wstring_view wordDelimiters) const
|
|
{
|
|
auto copy = pos;
|
|
auto bufferSize = GetSize();
|
|
|
|
// started on whitespace/delimiter, continue until the end of the previous word
|
|
while (_GetDelimiterClassAt(copy, wordDelimiters) != DelimiterClass::RegularChar)
|
|
{
|
|
if (!bufferSize.DecrementInBounds(copy))
|
|
{
|
|
// first char in buffer is a DelimiterChar or ControlChar
|
|
// there is no previous word
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// on a word, continue until the beginning of the word
|
|
while (_GetDelimiterClassAt(copy, wordDelimiters) == DelimiterClass::RegularChar)
|
|
{
|
|
if (!bufferSize.DecrementInBounds(copy))
|
|
{
|
|
// first char in buffer is a RegularChar
|
|
// there is no previous word
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// successful move, copy result out
|
|
pos = copy;
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update pos to be the beginning of the current glyph/character. This is used for accessibility
|
|
// Arguments:
|
|
// - pos - a COORD on the word you are currently on
|
|
// Return Value:
|
|
// - pos - The COORD for the first cell of the current glyph (inclusive)
|
|
const til::point TextBuffer::GetGlyphStart(const til::point pos) const
|
|
{
|
|
COORD resultPos = pos;
|
|
|
|
const auto bufferSize = GetSize();
|
|
if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr().IsTrailing())
|
|
{
|
|
bufferSize.DecrementInBounds(resultPos, true);
|
|
}
|
|
|
|
return resultPos;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update pos to be the end of the current glyph/character. This is used for accessibility
|
|
// Arguments:
|
|
// - pos - a COORD on the word you are currently on
|
|
// Return Value:
|
|
// - pos - The COORD for the last cell of the current glyph (exclusive)
|
|
const til::point TextBuffer::GetGlyphEnd(const til::point pos) const
|
|
{
|
|
COORD resultPos = pos;
|
|
|
|
const auto bufferSize = GetSize();
|
|
if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr().IsLeading())
|
|
{
|
|
bufferSize.IncrementInBounds(resultPos, true);
|
|
}
|
|
|
|
// increment one more time to become exclusive
|
|
bufferSize.IncrementInBounds(resultPos, true);
|
|
return resultPos;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update pos to be the beginning of the next glyph/character. This is used for accessibility
|
|
// Arguments:
|
|
// - pos - a COORD on the word you are currently on
|
|
// - allowBottomExclusive - allow the nonexistent end-of-buffer cell to be encountered
|
|
// Return Value:
|
|
// - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary)
|
|
// - pos - The COORD for the first cell of the current glyph (inclusive)
|
|
bool TextBuffer::MoveToNextGlyph(til::point& pos, bool allowBottomExclusive) const
|
|
{
|
|
COORD resultPos = pos;
|
|
|
|
// try to move. If we can't, we're done.
|
|
const auto bufferSize = GetSize();
|
|
const bool success = bufferSize.IncrementInBounds(resultPos, allowBottomExclusive);
|
|
if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr().IsTrailing())
|
|
{
|
|
bufferSize.IncrementInBounds(resultPos, allowBottomExclusive);
|
|
}
|
|
|
|
pos = resultPos;
|
|
return success;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Update pos to be the beginning of the previous glyph/character. This is used for accessibility
|
|
// Arguments:
|
|
// - pos - a COORD on the word you are currently on
|
|
// - allowBottomExclusive - allow the nonexistent end-of-buffer cell to be encountered
|
|
// Return Value:
|
|
// - true, if successfully updated pos. False, if we are unable to move (usually due to a buffer boundary)
|
|
// - pos - The COORD for the first cell of the previous glyph (inclusive)
|
|
bool TextBuffer::MoveToPreviousGlyph(til::point& pos, bool allowBottomExclusive) const
|
|
{
|
|
COORD resultPos = pos;
|
|
|
|
// try to move. If we can't, we're done.
|
|
const auto bufferSize = GetSize();
|
|
const bool success = bufferSize.DecrementInBounds(resultPos, allowBottomExclusive);
|
|
if (resultPos != bufferSize.EndExclusive() && GetCellDataAt(resultPos)->DbcsAttr().IsLeading())
|
|
{
|
|
bufferSize.DecrementInBounds(resultPos, allowBottomExclusive);
|
|
}
|
|
|
|
pos = resultPos;
|
|
return success;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Determines the line-by-line rectangles based on two COORDs
|
|
// - expands the rectangles to support wide glyphs
|
|
// - used for selection rects and UIA bounding rects
|
|
// Arguments:
|
|
// - start: a corner of the text region of interest (inclusive)
|
|
// - end: the other corner of the text region of interest (inclusive)
|
|
// - blockSelection: when enabled, only get the rectangular text region,
|
|
// as opposed to the text extending to the left/right
|
|
// buffer margins
|
|
// Return Value:
|
|
// - the delimiter class for the given char
|
|
const std::vector<SMALL_RECT> TextBuffer::GetTextRects(COORD start, COORD end, bool blockSelection) const
|
|
{
|
|
std::vector<SMALL_RECT> textRects;
|
|
|
|
const auto bufferSize = GetSize();
|
|
|
|
// (0,0) is the top-left of the screen
|
|
// the physically "higher" coordinate is closer to the top-left
|
|
// the physically "lower" coordinate is closer to the bottom-right
|
|
const auto [higherCoord, lowerCoord] = bufferSize.CompareInBounds(start, end) <= 0 ?
|
|
std::make_tuple(start, end) :
|
|
std::make_tuple(end, start);
|
|
|
|
const auto textRectSize = base::ClampedNumeric<short>(1) + lowerCoord.Y - higherCoord.Y;
|
|
textRects.reserve(textRectSize);
|
|
for (auto row = higherCoord.Y; row <= lowerCoord.Y; row++)
|
|
{
|
|
SMALL_RECT textRow;
|
|
|
|
textRow.Top = row;
|
|
textRow.Bottom = row;
|
|
|
|
if (blockSelection || higherCoord.Y == lowerCoord.Y)
|
|
{
|
|
// set the left and right margin to the left-/right-most respectively
|
|
textRow.Left = std::min(higherCoord.X, lowerCoord.X);
|
|
textRow.Right = std::max(higherCoord.X, lowerCoord.X);
|
|
}
|
|
else
|
|
{
|
|
textRow.Left = (row == higherCoord.Y) ? higherCoord.X : bufferSize.Left();
|
|
textRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : bufferSize.RightInclusive();
|
|
}
|
|
|
|
_ExpandTextRow(textRow);
|
|
textRects.emplace_back(textRow);
|
|
}
|
|
|
|
return textRects;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Expand the selection row according to include wide glyphs fully
|
|
// - this is particularly useful for box selections (ALT + selection)
|
|
// Arguments:
|
|
// - selectionRow: the selection row to be expanded
|
|
// Return Value:
|
|
// - modifies selectionRow's Left and Right values to expand properly
|
|
void TextBuffer::_ExpandTextRow(SMALL_RECT& textRow) const
|
|
{
|
|
const auto bufferSize = GetSize();
|
|
|
|
// expand left side of rect
|
|
COORD targetPoint{ textRow.Left, textRow.Top };
|
|
if (GetCellDataAt(targetPoint)->DbcsAttr().IsTrailing())
|
|
{
|
|
if (targetPoint.X == bufferSize.Left())
|
|
{
|
|
bufferSize.IncrementInBounds(targetPoint);
|
|
}
|
|
else
|
|
{
|
|
bufferSize.DecrementInBounds(targetPoint);
|
|
}
|
|
textRow.Left = targetPoint.X;
|
|
}
|
|
|
|
// expand right side of rect
|
|
targetPoint = { textRow.Right, textRow.Bottom };
|
|
if (GetCellDataAt(targetPoint)->DbcsAttr().IsLeading())
|
|
{
|
|
if (targetPoint.X == bufferSize.RightInclusive())
|
|
{
|
|
bufferSize.DecrementInBounds(targetPoint);
|
|
}
|
|
else
|
|
{
|
|
bufferSize.IncrementInBounds(targetPoint);
|
|
}
|
|
textRow.Right = targetPoint.X;
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Retrieves the text data from the selected region and presents it in a clipboard-ready format (given little post-processing).
|
|
// Arguments:
|
|
// - includeCRLF - inject CRLF pairs to the end of each line
|
|
// - trimTrailingWhitespace - remove the trailing whitespace at the end of each line
|
|
// - textRects - the rectangular regions from which the data will be extracted from the buffer (i.e.: selection rects)
|
|
// - GetAttributeColors - function used to map TextAttribute to RGB COLORREFs. If null, only extract the text.
|
|
// Return Value:
|
|
// - The text, background color, and foreground color data of the selected region of the text buffer.
|
|
const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF,
|
|
const bool trimTrailingWhitespace,
|
|
const std::vector<SMALL_RECT>& selectionRects,
|
|
std::function<std::pair<COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const
|
|
{
|
|
TextAndColor data;
|
|
const bool copyTextColor = GetAttributeColors != nullptr;
|
|
|
|
// preallocate our vectors to reduce reallocs
|
|
size_t const rows = selectionRects.size();
|
|
data.text.reserve(rows);
|
|
if (copyTextColor)
|
|
{
|
|
data.FgAttr.reserve(rows);
|
|
data.BkAttr.reserve(rows);
|
|
}
|
|
|
|
// for each row in the selection
|
|
for (UINT i = 0; i < rows; i++)
|
|
{
|
|
const UINT iRow = selectionRects.at(i).Top;
|
|
|
|
const Viewport highlight = Viewport::FromInclusive(selectionRects.at(i));
|
|
|
|
// retrieve the data from the screen buffer
|
|
auto it = GetCellDataAt(highlight.Origin(), highlight);
|
|
|
|
// allocate a string buffer
|
|
std::wstring selectionText;
|
|
std::vector<COLORREF> selectionFgAttr;
|
|
std::vector<COLORREF> selectionBkAttr;
|
|
|
|
// preallocate to avoid reallocs
|
|
selectionText.reserve(gsl::narrow<size_t>(highlight.Width()) + 2); // + 2 for \r\n if we munged it
|
|
if (copyTextColor)
|
|
{
|
|
selectionFgAttr.reserve(gsl::narrow<size_t>(highlight.Width()) + 2);
|
|
selectionBkAttr.reserve(gsl::narrow<size_t>(highlight.Width()) + 2);
|
|
}
|
|
|
|
// copy char data into the string buffer, skipping trailing bytes
|
|
while (it)
|
|
{
|
|
const auto& cell = *it;
|
|
|
|
if (!cell.DbcsAttr().IsTrailing())
|
|
{
|
|
selectionText.append(cell.Chars());
|
|
|
|
if (copyTextColor)
|
|
{
|
|
const auto cellData = cell.TextAttr();
|
|
const auto [CellFgAttr, CellBkAttr] = GetAttributeColors(cellData);
|
|
for (const wchar_t wch : cell.Chars())
|
|
{
|
|
selectionFgAttr.push_back(CellFgAttr);
|
|
selectionBkAttr.push_back(CellBkAttr);
|
|
}
|
|
}
|
|
}
|
|
#pragma warning(suppress : 26444)
|
|
// TODO GH 2675: figure out why there's custom construction/destruction happening here
|
|
it++;
|
|
}
|
|
|
|
const bool forcedWrap = GetRowByOffset(iRow).GetCharRow().WasWrapForced();
|
|
|
|
if (trimTrailingWhitespace)
|
|
{
|
|
// if the row was NOT wrapped...
|
|
if (!forcedWrap)
|
|
{
|
|
// remove the spaces at the end (aka trim the trailing whitespace)
|
|
while (!selectionText.empty() && selectionText.back() == UNICODE_SPACE)
|
|
{
|
|
selectionText.pop_back();
|
|
if (copyTextColor)
|
|
{
|
|
selectionFgAttr.pop_back();
|
|
selectionBkAttr.pop_back();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// apply CR/LF to the end of the final string, unless we're the last line.
|
|
// a.k.a if we're earlier than the bottom, then apply CR/LF.
|
|
if (includeCRLF && i < selectionRects.size() - 1)
|
|
{
|
|
// if the row was NOT wrapped...
|
|
if (!forcedWrap)
|
|
{
|
|
// then we can assume a CR/LF is proper
|
|
selectionText.push_back(UNICODE_CARRIAGERETURN);
|
|
selectionText.push_back(UNICODE_LINEFEED);
|
|
|
|
if (copyTextColor)
|
|
{
|
|
// cant see CR/LF so just use black FG & BK
|
|
COLORREF const Blackness = RGB(0x00, 0x00, 0x00);
|
|
selectionFgAttr.push_back(Blackness);
|
|
selectionFgAttr.push_back(Blackness);
|
|
selectionBkAttr.push_back(Blackness);
|
|
selectionBkAttr.push_back(Blackness);
|
|
}
|
|
}
|
|
}
|
|
|
|
data.text.emplace_back(std::move(selectionText));
|
|
if (copyTextColor)
|
|
{
|
|
data.FgAttr.emplace_back(std::move(selectionFgAttr));
|
|
data.BkAttr.emplace_back(std::move(selectionBkAttr));
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Generates a CF_HTML compliant structure based on the passed in text and color data
|
|
// Arguments:
|
|
// - rows - the text and color data we will format & encapsulate
|
|
// - backgroundColor - default background color for characters, also used in padding
|
|
// - fontHeightPoints - the unscaled font height
|
|
// - fontFaceName - the name of the font used
|
|
// Return Value:
|
|
// - string containing the generated HTML
|
|
std::string TextBuffer::GenHTML(const TextAndColor& rows,
|
|
const int fontHeightPoints,
|
|
const std::wstring_view fontFaceName,
|
|
const COLORREF backgroundColor)
|
|
{
|
|
try
|
|
{
|
|
std::ostringstream htmlBuilder;
|
|
|
|
// First we have to add some standard
|
|
// HTML boiler plate required for CF_HTML
|
|
// as part of the HTML Clipboard format
|
|
const std::string htmlHeader =
|
|
"<!DOCTYPE><HTML><HEAD></HEAD><BODY>";
|
|
htmlBuilder << htmlHeader;
|
|
|
|
htmlBuilder << "<!--StartFragment -->";
|
|
|
|
// apply global style in div element
|
|
{
|
|
htmlBuilder << "<DIV STYLE=\"";
|
|
htmlBuilder << "display:inline-block;";
|
|
htmlBuilder << "white-space:pre;";
|
|
|
|
htmlBuilder << "background-color:";
|
|
htmlBuilder << Utils::ColorToHexString(backgroundColor);
|
|
htmlBuilder << ";";
|
|
|
|
htmlBuilder << "font-family:";
|
|
htmlBuilder << "'";
|
|
htmlBuilder << ConvertToA(CP_UTF8, fontFaceName);
|
|
htmlBuilder << "',";
|
|
// even with different font, add monospace as fallback
|
|
htmlBuilder << "monospace;";
|
|
|
|
htmlBuilder << "font-size:";
|
|
htmlBuilder << fontHeightPoints;
|
|
htmlBuilder << "pt;";
|
|
|
|
// note: MS Word doesn't support padding (in this way at least)
|
|
htmlBuilder << "padding:";
|
|
htmlBuilder << 4; // todo: customizable padding
|
|
htmlBuilder << "px;";
|
|
|
|
htmlBuilder << "\">";
|
|
}
|
|
|
|
// copy text and info color from buffer
|
|
bool hasWrittenAnyText = false;
|
|
std::optional<COLORREF> fgColor = std::nullopt;
|
|
std::optional<COLORREF> bkColor = std::nullopt;
|
|
for (size_t row = 0; row < rows.text.size(); row++)
|
|
{
|
|
size_t startOffset = 0;
|
|
|
|
if (row != 0)
|
|
{
|
|
htmlBuilder << "<BR>";
|
|
}
|
|
|
|
for (size_t col = 0; col < rows.text.at(row).length(); col++)
|
|
{
|
|
const auto writeAccumulatedChars = [&](bool includeCurrent) {
|
|
if (col >= startOffset)
|
|
{
|
|
const auto unescapedText = ConvertToA(CP_UTF8, std::wstring_view(rows.text.at(row)).substr(startOffset, col - startOffset + includeCurrent));
|
|
for (const auto c : unescapedText)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '<':
|
|
htmlBuilder << "<";
|
|
break;
|
|
case '>':
|
|
htmlBuilder << ">";
|
|
break;
|
|
case '&':
|
|
htmlBuilder << "&";
|
|
break;
|
|
default:
|
|
htmlBuilder << c;
|
|
}
|
|
}
|
|
|
|
startOffset = col;
|
|
}
|
|
};
|
|
|
|
if (rows.text.at(row).at(col) == '\r' || rows.text.at(row).at(col) == '\n')
|
|
{
|
|
// do not include \r nor \n as they don't have color attributes
|
|
// and are not HTML friendly. For line break use '<BR>' instead.
|
|
writeAccumulatedChars(false);
|
|
break;
|
|
}
|
|
|
|
bool colorChanged = false;
|
|
if (!fgColor.has_value() || rows.FgAttr.at(row).at(col) != fgColor.value())
|
|
{
|
|
fgColor = rows.FgAttr.at(row).at(col);
|
|
colorChanged = true;
|
|
}
|
|
|
|
if (!bkColor.has_value() || rows.BkAttr.at(row).at(col) != bkColor.value())
|
|
{
|
|
bkColor = rows.BkAttr.at(row).at(col);
|
|
colorChanged = true;
|
|
}
|
|
|
|
if (colorChanged)
|
|
{
|
|
writeAccumulatedChars(false);
|
|
|
|
if (hasWrittenAnyText)
|
|
{
|
|
htmlBuilder << "</SPAN>";
|
|
}
|
|
|
|
htmlBuilder << "<SPAN STYLE=\"";
|
|
htmlBuilder << "color:";
|
|
htmlBuilder << Utils::ColorToHexString(fgColor.value());
|
|
htmlBuilder << ";";
|
|
htmlBuilder << "background-color:";
|
|
htmlBuilder << Utils::ColorToHexString(bkColor.value());
|
|
htmlBuilder << ";";
|
|
htmlBuilder << "\">";
|
|
}
|
|
|
|
hasWrittenAnyText = true;
|
|
|
|
// if this is the last character in the row, flush the whole row
|
|
if (col == rows.text.at(row).length() - 1)
|
|
{
|
|
writeAccumulatedChars(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasWrittenAnyText)
|
|
{
|
|
// last opened span wasn't closed in loop above, so close it now
|
|
htmlBuilder << "</SPAN>";
|
|
}
|
|
|
|
htmlBuilder << "</DIV>";
|
|
|
|
htmlBuilder << "<!--EndFragment -->";
|
|
|
|
constexpr std::string_view HtmlFooter = "</BODY></HTML>";
|
|
htmlBuilder << HtmlFooter;
|
|
|
|
// once filled with values, there will be exactly 157 bytes in the clipboard header
|
|
constexpr size_t ClipboardHeaderSize = 157;
|
|
|
|
// these values are byte offsets from start of clipboard
|
|
const size_t htmlStartPos = ClipboardHeaderSize;
|
|
const size_t htmlEndPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlBuilder.tellp());
|
|
const size_t fragStartPos = ClipboardHeaderSize + gsl::narrow<size_t>(htmlHeader.length());
|
|
const size_t fragEndPos = htmlEndPos - HtmlFooter.length();
|
|
|
|
// header required by HTML 0.9 format
|
|
std::ostringstream clipHeaderBuilder;
|
|
clipHeaderBuilder << "Version:0.9\r\n";
|
|
clipHeaderBuilder << std::setfill('0');
|
|
clipHeaderBuilder << "StartHTML:" << std::setw(10) << htmlStartPos << "\r\n";
|
|
clipHeaderBuilder << "EndHTML:" << std::setw(10) << htmlEndPos << "\r\n";
|
|
clipHeaderBuilder << "StartFragment:" << std::setw(10) << fragStartPos << "\r\n";
|
|
clipHeaderBuilder << "EndFragment:" << std::setw(10) << fragEndPos << "\r\n";
|
|
clipHeaderBuilder << "StartSelection:" << std::setw(10) << fragStartPos << "\r\n";
|
|
clipHeaderBuilder << "EndSelection:" << std::setw(10) << fragEndPos << "\r\n";
|
|
|
|
return clipHeaderBuilder.str() + htmlBuilder.str();
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_HR(wil::ResultFromCaughtException());
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Generates an RTF document based on the passed in text and color data
|
|
// RTF 1.5 Spec: https://www.biblioscape.com/rtf15_spec.htm
|
|
// Arguments:
|
|
// - rows - the text and color data we will format & encapsulate
|
|
// - backgroundColor - default background color for characters, also used in padding
|
|
// - fontHeightPoints - the unscaled font height
|
|
// - fontFaceName - the name of the font used
|
|
// - htmlTitle - value used in title tag of html header. Used to name the application
|
|
// Return Value:
|
|
// - string containing the generated RTF
|
|
std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoints, const std::wstring_view fontFaceName, const COLORREF backgroundColor)
|
|
{
|
|
try
|
|
{
|
|
std::ostringstream rtfBuilder;
|
|
|
|
// start rtf
|
|
rtfBuilder << "{";
|
|
|
|
// Standard RTF header.
|
|
// This is similar to the header generated by WordPad.
|
|
// \ansi - specifies that the ANSI char set is used in the current doc
|
|
// \ansicpg1252 - represents the ANSI code page which is used to perform the Unicode to ANSI conversion when writing RTF text
|
|
// \deff0 - specifies that the default font for the document is the one at index 0 in the font table
|
|
// \nouicompat - ?
|
|
rtfBuilder << "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat";
|
|
|
|
// font table
|
|
rtfBuilder << "{\\fonttbl{\\f0\\fmodern\\fcharset0 " << ConvertToA(CP_UTF8, fontFaceName) << ";}}";
|
|
|
|
// map to keep track of colors:
|
|
// keys are colors represented by COLORREF
|
|
// values are indices of the corresponding colors in the color table
|
|
std::unordered_map<COLORREF, int> colorMap;
|
|
int nextColorIndex = 1; // leave 0 for the default color and start from 1.
|
|
|
|
// RTF color table
|
|
std::ostringstream colorTableBuilder;
|
|
colorTableBuilder << "{\\colortbl ;";
|
|
colorTableBuilder << "\\red" << static_cast<int>(GetRValue(backgroundColor))
|
|
<< "\\green" << static_cast<int>(GetGValue(backgroundColor))
|
|
<< "\\blue" << static_cast<int>(GetBValue(backgroundColor))
|
|
<< ";";
|
|
colorMap[backgroundColor] = nextColorIndex++;
|
|
|
|
// content
|
|
std::ostringstream contentBuilder;
|
|
contentBuilder << "\\viewkind4\\uc4";
|
|
|
|
// paragraph styles
|
|
// \fs specifies font size in half-points i.e. \fs20 results in a font size
|
|
// of 10 pts. That's why, font size is multiplied by 2 here.
|
|
contentBuilder << "\\pard\\slmult1\\f0\\fs" << std::to_string(2 * fontHeightPoints)
|
|
<< "\\highlight1"
|
|
<< " ";
|
|
|
|
std::optional<COLORREF> fgColor = std::nullopt;
|
|
std::optional<COLORREF> bkColor = std::nullopt;
|
|
for (size_t row = 0; row < rows.text.size(); ++row)
|
|
{
|
|
size_t startOffset = 0;
|
|
|
|
if (row != 0)
|
|
{
|
|
contentBuilder << "\\line "; // new line
|
|
}
|
|
|
|
for (size_t col = 0; col < rows.text.at(row).length(); ++col)
|
|
{
|
|
const auto writeAccumulatedChars = [&](bool includeCurrent) {
|
|
if (col >= startOffset)
|
|
{
|
|
const auto unescapedText = ConvertToA(CP_UTF8, std::wstring_view(rows.text.at(row)).substr(startOffset, col - startOffset + includeCurrent));
|
|
for (const auto c : unescapedText)
|
|
{
|
|
switch (c)
|
|
{
|
|
case '\\':
|
|
case '{':
|
|
case '}':
|
|
contentBuilder << "\\" << c;
|
|
break;
|
|
default:
|
|
contentBuilder << c;
|
|
}
|
|
}
|
|
|
|
startOffset = col;
|
|
}
|
|
};
|
|
|
|
if (rows.text.at(row).at(col) == '\r' || rows.text.at(row).at(col) == '\n')
|
|
{
|
|
// do not include \r nor \n as they don't have color attributes.
|
|
// For line break use \line instead.
|
|
writeAccumulatedChars(false);
|
|
break;
|
|
}
|
|
|
|
bool colorChanged = false;
|
|
if (!fgColor.has_value() || rows.FgAttr.at(row).at(col) != fgColor.value())
|
|
{
|
|
fgColor = rows.FgAttr.at(row).at(col);
|
|
colorChanged = true;
|
|
}
|
|
|
|
if (!bkColor.has_value() || rows.BkAttr.at(row).at(col) != bkColor.value())
|
|
{
|
|
bkColor = rows.BkAttr.at(row).at(col);
|
|
colorChanged = true;
|
|
}
|
|
|
|
if (colorChanged)
|
|
{
|
|
writeAccumulatedChars(false);
|
|
|
|
int bkColorIndex = 0;
|
|
if (colorMap.find(bkColor.value()) != colorMap.end())
|
|
{
|
|
// color already exists in the map, just retrieve the index
|
|
bkColorIndex = colorMap[bkColor.value()];
|
|
}
|
|
else
|
|
{
|
|
// color not present in the map, so add it
|
|
colorTableBuilder << "\\red" << static_cast<int>(GetRValue(bkColor.value()))
|
|
<< "\\green" << static_cast<int>(GetGValue(bkColor.value()))
|
|
<< "\\blue" << static_cast<int>(GetBValue(bkColor.value()))
|
|
<< ";";
|
|
colorMap[bkColor.value()] = nextColorIndex;
|
|
bkColorIndex = nextColorIndex++;
|
|
}
|
|
|
|
int fgColorIndex = 0;
|
|
if (colorMap.find(fgColor.value()) != colorMap.end())
|
|
{
|
|
// color already exists in the map, just retrieve the index
|
|
fgColorIndex = colorMap[fgColor.value()];
|
|
}
|
|
else
|
|
{
|
|
// color not present in the map, so add it
|
|
colorTableBuilder << "\\red" << static_cast<int>(GetRValue(fgColor.value()))
|
|
<< "\\green" << static_cast<int>(GetGValue(fgColor.value()))
|
|
<< "\\blue" << static_cast<int>(GetBValue(fgColor.value()))
|
|
<< ";";
|
|
colorMap[fgColor.value()] = nextColorIndex;
|
|
fgColorIndex = nextColorIndex++;
|
|
}
|
|
|
|
contentBuilder << "\\highlight" << bkColorIndex
|
|
<< "\\cf" << fgColorIndex
|
|
<< " ";
|
|
}
|
|
|
|
// if this is the last character in the row, flush the whole row
|
|
if (col == rows.text.at(row).length() - 1)
|
|
{
|
|
writeAccumulatedChars(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// end colortbl
|
|
colorTableBuilder << "}";
|
|
|
|
// add color table to the final RTF
|
|
rtfBuilder << colorTableBuilder.str();
|
|
|
|
// add the text content to the final RTF
|
|
rtfBuilder << contentBuilder.str();
|
|
|
|
// end rtf
|
|
rtfBuilder << "}";
|
|
|
|
return rtfBuilder.str();
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_HR(wil::ResultFromCaughtException());
|
|
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
|
|
// - lastCharacterViewport - Optional. If the caller knows that the last
|
|
// nonspace character is in a particular Viewport, the caller can provide this
|
|
// parameter as an optimization, as opposed to searching the entire buffer.
|
|
// - positionInfo - Optional. The caller can provide a pair of rows in this
|
|
// parameter and we'll calculate the position of the _end_ of those rows in
|
|
// the new buffer. The rows's new value is placed back into this parameter.
|
|
// 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,
|
|
const std::optional<Viewport> lastCharacterViewport,
|
|
std::optional<std::reference_wrapper<PositionInformation>> positionInfo)
|
|
{
|
|
const Cursor& oldCursor = oldBuffer.GetCursor();
|
|
Cursor& newCursor = newBuffer.GetCursor();
|
|
|
|
// 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(lastCharacterViewport);
|
|
|
|
const short cOldRowsTotal = cOldLastChar.Y + 1;
|
|
const short cOldColsTotal = oldBuffer.GetSize().Width();
|
|
|
|
COORD cNewCursorPos = { 0 };
|
|
bool fFoundCursorPos = false;
|
|
bool foundOldMutable = false;
|
|
bool foundOldVisible = 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 we found the old row that the caller was interested in, set the
|
|
// out value of that parameter to the cursor's current Y position (the
|
|
// new location of the _end_ of that row in the buffer).
|
|
if (positionInfo.has_value())
|
|
{
|
|
if (!foundOldMutable)
|
|
{
|
|
if (iOldRow >= positionInfo.value().get().mutableViewportTop)
|
|
{
|
|
positionInfo.value().get().mutableViewportTop = newCursor.GetPosition().Y;
|
|
foundOldMutable = true;
|
|
}
|
|
}
|
|
|
|
if (!foundOldVisible)
|
|
{
|
|
if (iOldRow >= positionInfo.value().get().visibleViewportTop)
|
|
{
|
|
positionInfo.value().get().visibleViewportTop = newCursor.GetPosition().Y;
|
|
foundOldVisible = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
return hr;
|
|
}
|