Move rect expansion to textbuffer; refactor selection code (#4560)

- When performing chunk selection, the expansion now occurs at the time
  of the selection, not the rendering of the selection
- `GetSelectionRects()` was moved to the `TextBuffer` and is now shared
  between ConHost and Windows Terminal
- Some of the selection variables were renamed for clarity
- Selection COORDs are now in the Text Buffer coordinate space
- Fixes an issue with Shift+Click after performing a Multi-Click
  Selection

## References
This also contributes to...
- #4509: UIA Box Selection
- #2447: UIA Signaling for Selection
- #1354: UIA support for Wide Glyphs

Now that the expansion occurs at before render-time, the selection
anchors are an accurate representation of what is selected. We just need
to move `GetText` to the `TextBuffer`. Then we can have those three
issues just rely on code from the text buffer. This also means ConHost
gets some of this stuff for free 😀

### TextBuffer
- `GetTextRects` is the abstracted form of `GetSelectionRects`
- `_ExpandTextRow` is still needed to handle wide glyphs properly

### Terminal
- Rename...
    - `_boxSelection` --> `_blockSelection` for consistency with ConHost
    - `_selectionAnchor` --> `_selectionStart` for consistency with UIA
    - `_endSelectionPosition` --> `_selectionEnd` for consistency with
      UIA
- Selection anchors are in Text Buffer coordinates now
- Really rely on `SetSelectionEnd` to accomplish appropriate chunk
  selection and shift+click actions

## Validation Steps Performed
- Shift+Click
- Multi-Click --> Shift+Click
- Chunk Selection at...
    - top of buffer
    - bottom of buffer
    - random region in scrollback

Closes #4465
Closes #4547
This commit is contained in:
Carlos Zamora 2020-02-27 16:42:26 -08:00 committed by GitHub
parent 74cd9db383
commit 0e672fac08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 395 additions and 529 deletions

View file

@ -1314,6 +1314,101 @@ TextBuffer::DelimiterClass TextBuffer::_GetDelimiterClass(const std::wstring_vie
}
}
// 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:

View file

@ -135,6 +135,8 @@ public:
bool MoveToNextWord(COORD& pos, const std::wstring_view wordDelimiters, COORD lastCharPos) const;
bool MoveToPreviousWord(COORD& pos, const std::wstring_view wordDelimiters) const;
const std::vector<SMALL_RECT> GetTextRects(COORD start, COORD end, bool blockSelection = false) const;
class TextAndColor
{
public:
@ -193,6 +195,8 @@ private:
ROW& _GetFirstRow();
ROW& _GetPrevRowNoWrap(const ROW& row);
void _ExpandTextRow(SMALL_RECT& selectionRow) const;
enum class DelimiterClass
{
ControlChar,

View file

@ -334,7 +334,7 @@ HRESULT _stdcall TerminalStartSelection(void* terminal, COORD cursorPosition, bo
terminalPosition.Y /= fontSize.Y;
publicTerminal->_terminal->SetSelectionAnchor(terminalPosition);
publicTerminal->_terminal->SetBoxSelection(altPressed);
publicTerminal->_terminal->SetBlockSelection(altPressed);
publicTerminal->_renderer->TriggerSelection();
@ -354,7 +354,7 @@ HRESULT _stdcall TerminalMoveSelection(void* terminal, COORD cursorPosition)
terminalPosition.X /= fontSize.X;
terminalPosition.Y /= fontSize.Y;
publicTerminal->_terminal->SetEndSelectionPosition(terminalPosition);
publicTerminal->_terminal->SetSelectionEnd(terminalPosition);
publicTerminal->_renderer->TriggerSelection();
return S_OK;

View file

@ -142,7 +142,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
auto lock = _terminal->LockForWriting();
if (search.FindNext())
{
_terminal->SetBoxSelection(false);
_terminal->SetBlockSelection(false);
search.Select();
_renderer->TriggerSelection();
}
@ -817,7 +817,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
const auto terminalPosition = _GetTerminalPosition(cursorPosition);
// handle ALT key
_terminal->SetBoxSelection(altEnabled);
_terminal->SetBlockSelection(altEnabled);
auto clickCount = _NumberOfClicks(cursorPosition, point.Timestamp());
@ -828,17 +828,17 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
if (multiClickMapper == 3)
{
_terminal->TripleClickSelection(terminalPosition);
_terminal->MultiClickSelection(terminalPosition, ::Terminal::SelectionExpansionMode::Line);
}
else if (multiClickMapper == 2)
{
_terminal->DoubleClickSelection(terminalPosition);
_terminal->MultiClickSelection(terminalPosition, ::Terminal::SelectionExpansionMode::Word);
}
else
{
if (shiftEnabled && _terminal->IsSelectionActive())
{
_terminal->SetEndSelectionPosition(terminalPosition);
_terminal->SetSelectionEnd(terminalPosition, ::Terminal::SelectionExpansionMode::Cell);
}
else
{
@ -1474,7 +1474,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
terminalPosition.X = std::clamp<short>(terminalPosition.X, 0, lastVisibleCol);
// save location (for rendering) + render
_terminal->SetEndSelectionPosition(terminalPosition);
_terminal->SetSelectionEnd(terminalPosition);
_renderer->TriggerSelection();
}

View file

@ -44,12 +44,10 @@ Terminal::Terminal() :
_pfnWriteInput{ nullptr },
_scrollOffset{ 0 },
_snapOnInput{ true },
_boxSelection{ false },
_selectionActive{ false },
_blockSelection{ false },
_selection{ std::nullopt },
_allowSingleCharSelection{ true },
_copyOnSelect{ false },
_selectionAnchor{ 0, 0 },
_endSelectionPosition{ 0, 0 }
_copyOnSelect{ false }
{
auto dispatch = std::make_unique<TerminalDispatch>(*this);
auto engine = std::make_unique<OutputStateMachineEngine>(std::move(dispatch));

View file

@ -142,7 +142,7 @@ public:
void ClearSelection() override;
void SelectNewRegion(const COORD coordStart, const COORD coordEnd) override;
const COORD GetSelectionAnchor() const noexcept override;
const COORD GetEndSelectionPosition() const noexcept override;
const COORD GetSelectionEnd() const noexcept override;
const std::wstring GetConsoleTitle() const noexcept override;
void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute) override;
#pragma endregion
@ -157,12 +157,17 @@ public:
#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
enum class SelectionExpansionMode
{
Cell,
Word,
Line
};
const bool IsCopyOnSelectActive() const noexcept;
void DoubleClickSelection(const COORD position);
void TripleClickSelection(const COORD position);
void MultiClickSelection(const COORD viewportPos, SelectionExpansionMode expansionMode);
void SetSelectionAnchor(const COORD position);
void SetEndSelectionPosition(const COORD position);
void SetBoxSelection(const bool isEnabled) noexcept;
void SetSelectionEnd(const COORD position, std::optional<SelectionExpansionMode> newExpansionMode = std::nullopt);
void SetBlockSelection(const bool isEnabled) noexcept;
const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace) const;
#pragma endregion
@ -187,19 +192,20 @@ private:
bool _suppressApplicationTitle;
#pragma region Text Selection
enum class SelectionExpansionMode
// a selection is represented as a range between two COORDs (start and end)
// the pivot is the COORD that remains selected when you extend a selection in any direction
// this is particularly useful when a word selection is extended over its starting point
// see TerminalSelection.cpp for more information
struct SelectionAnchors
{
Cell,
Word,
Line
COORD start;
COORD end;
COORD pivot;
};
COORD _selectionAnchor;
COORD _endSelectionPosition;
bool _boxSelection;
bool _selectionActive;
std::optional<SelectionAnchors> _selection;
bool _blockSelection;
bool _allowSingleCharSelection;
bool _copyOnSelect;
SHORT _selectionVerticalOffset;
std::wstring _wordDelimiters;
SelectionExpansionMode _multiClickSelectionMode;
#pragma endregion
@ -248,15 +254,10 @@ private:
#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
std::vector<SMALL_RECT> _GetSelectionRects() const noexcept;
SHORT _ExpandWideGlyphSelectionLeft(const SHORT xPos, const SHORT yPos) const;
SHORT _ExpandWideGlyphSelectionRight(const SHORT xPos, const SHORT yPos) const;
COORD _ExpandDoubleClickSelectionLeft(const COORD position) const;
COORD _ExpandDoubleClickSelectionRight(const COORD position) const;
std::pair<COORD, COORD> _PivotSelection(const COORD targetPos) const;
std::pair<COORD, COORD> _ExpandSelectionAnchors(std::pair<COORD, COORD> anchors) const;
COORD _ConvertToBufferCell(const COORD viewportPos) const;
const bool _IsSingleCellSelection() const noexcept;
std::tuple<COORD, COORD> _PreprocessSelectionCoords() const;
SMALL_RECT _GetSelectionRow(const SHORT row, const COORD higherCoord, const COORD lowerCoord) const;
void _ExpandSelectionRow(SMALL_RECT& selectionRow) const;
#pragma endregion
#ifdef UNIT_TESTING

View file

@ -7,6 +7,38 @@
using namespace Microsoft::Terminal::Core;
/* Selection Pivot Description:
* The pivot helps properly update the selection when a user moves a selection over itself
* See SelectionTest::DoubleClickDrag_Left for an example of the functionality mentioned here
* As an example, consider the following scenario...
* 1. Perform a word selection (double-click) on a word
*
* |-position where we double-clicked
* _|_
* |word|
* |--|
* start & pivot-| |-end
*
* 2. Drag your mouse down a line
*
*
* start & pivot-|__________
* __|word_______|
* |______|
* |
* |-end & mouse position
*
* 3. Drag your mouse up two lines
*
* |-start & mouse position
* |________
* ____| ______|
* |___w|ord
* |-end & pivot
*
* The pivot never moves until a new selection is created. It ensures that that cell will always be selected.
*/
// Method Description:
// - Helper to determine the selected region of the buffer. Used for rendering.
// Return Value:
@ -22,91 +54,12 @@ std::vector<SMALL_RECT> Terminal::_GetSelectionRects() const noexcept
try
{
// NOTE: (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] = _PreprocessSelectionCoords();
SHORT selectionRectSize;
THROW_IF_FAILED(ShortSub(lowerCoord.Y, higherCoord.Y, &selectionRectSize));
THROW_IF_FAILED(ShortAdd(selectionRectSize, 1, &selectionRectSize));
std::vector<SMALL_RECT> selectionArea;
selectionArea.reserve(selectionRectSize);
for (auto row = higherCoord.Y; row <= lowerCoord.Y; row++)
{
SMALL_RECT selectionRow = _GetSelectionRow(row, higherCoord, lowerCoord);
_ExpandSelectionRow(selectionRow);
selectionArea.emplace_back(selectionRow);
}
result.swap(selectionArea);
return _buffer->GetTextRects(_selection->start, _selection->end, _blockSelection);
}
CATCH_LOG();
return result;
}
// Method Description:
// - convert selection anchors to proper coordinates for rendering
// NOTE: (0,0) is top-left so vertical comparison is inverted
// Arguments:
// - None
// Return Value:
// - tuple.first: the physically "higher" coordinate (closer to the top-left)
// - tuple.second: the physically "lower" coordinate (closer to the bottom-right)
std::tuple<COORD, COORD> Terminal::_PreprocessSelectionCoords() const
{
// create these new anchors for comparison and rendering
COORD selectionAnchorWithOffset{ _selectionAnchor };
COORD endSelectionPositionWithOffset{ _endSelectionPosition };
// Add anchor offset here to update properly on new buffer output
THROW_IF_FAILED(ShortAdd(selectionAnchorWithOffset.Y, _selectionVerticalOffset, &selectionAnchorWithOffset.Y));
THROW_IF_FAILED(ShortAdd(endSelectionPositionWithOffset.Y, _selectionVerticalOffset, &endSelectionPositionWithOffset.Y));
// clamp anchors to be within buffer bounds
const auto bufferSize = _buffer->GetSize();
bufferSize.Clamp(selectionAnchorWithOffset);
bufferSize.Clamp(endSelectionPositionWithOffset);
// NOTE: (0,0) is top-left so vertical comparison is inverted
// CompareInBounds returns whether A is to the left of (rv<0), equal to (rv==0), or to the right of (rv>0) B.
// Here, we want the "left"most coordinate to be the one "higher" on the screen. The other gets the dubious honor of
// being the "lower."
return bufferSize.CompareInBounds(selectionAnchorWithOffset, endSelectionPositionWithOffset) <= 0 ?
std::make_tuple(selectionAnchorWithOffset, endSelectionPositionWithOffset) :
std::make_tuple(endSelectionPositionWithOffset, selectionAnchorWithOffset);
}
// Method Description:
// - constructs the selection row at the given row
// NOTE: (0,0) is top-left so vertical comparison is inverted
// Arguments:
// - row: the buffer y-value under observation
// - higherCoord: the physically "higher" coordinate (closer to the top-left)
// - lowerCoord: the physically "lower" coordinate (closer to the bottom-right)
// Return Value:
// - the selection row needed for rendering
SMALL_RECT Terminal::_GetSelectionRow(const SHORT row, const COORD higherCoord, const COORD lowerCoord) const
{
SMALL_RECT selectionRow;
selectionRow.Top = row;
selectionRow.Bottom = row;
if (_boxSelection || higherCoord.Y == lowerCoord.Y)
{
selectionRow.Left = std::min(higherCoord.X, lowerCoord.X);
selectionRow.Right = std::max(higherCoord.X, lowerCoord.X);
}
else
{
selectionRow.Left = (row == higherCoord.Y) ? higherCoord.X : _buffer->GetSize().Left();
selectionRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : _buffer->GetSize().RightInclusive();
}
return selectionRow;
}
// Method Description:
// - Get the current anchor position relative to the whole text buffer
// Arguments:
@ -115,9 +68,7 @@ SMALL_RECT Terminal::_GetSelectionRow(const SHORT row, const COORD higherCoord,
// - None
const COORD Terminal::GetSelectionAnchor() const noexcept
{
COORD selectionAnchorPos{ _selectionAnchor };
selectionAnchorPos.Y = base::ClampAdd(selectionAnchorPos.Y, _selectionVerticalOffset);
return selectionAnchorPos;
return _selection->start;
}
// Method Description:
@ -126,91 +77,9 @@ const COORD Terminal::GetSelectionAnchor() const noexcept
// - None
// Return Value:
// - None
const COORD Terminal::GetEndSelectionPosition() const noexcept
const COORD Terminal::GetSelectionEnd() const noexcept
{
COORD endSelectionPos{ _endSelectionPosition };
endSelectionPos.Y = base::ClampAdd(endSelectionPos.Y, _selectionVerticalOffset);
return endSelectionPos;
}
// Method Description:
// - Expand the selection row according to selection mode and wide glyphs
// - 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 Terminal::_ExpandSelectionRow(SMALL_RECT& selectionRow) const
{
const auto row = selectionRow.Top;
// expand selection for Double/Triple Click
if (_multiClickSelectionMode == SelectionExpansionMode::Word)
{
selectionRow.Left = _ExpandDoubleClickSelectionLeft({ selectionRow.Left, row }).X;
selectionRow.Right = _ExpandDoubleClickSelectionRight({ selectionRow.Right, row }).X;
}
else if (_multiClickSelectionMode == SelectionExpansionMode::Line)
{
selectionRow.Left = _buffer->GetSize().Left();
selectionRow.Right = _buffer->GetSize().RightInclusive();
}
// expand selection for Wide Glyphs
selectionRow.Left = _ExpandWideGlyphSelectionLeft(selectionRow.Left, row);
selectionRow.Right = _ExpandWideGlyphSelectionRight(selectionRow.Right, row);
}
// Method Description:
// - Expands the selection left-wards to cover a wide glyph, if necessary
// Arguments:
// - position: the (x,y) coordinate on the visible viewport
// Return Value:
// - updated x position to encapsulate the wide glyph
SHORT Terminal::_ExpandWideGlyphSelectionLeft(const SHORT xPos, const SHORT yPos) const
{
// don't change the value if at/outside the boundary
const auto bufferSize = _buffer->GetSize();
if (xPos <= bufferSize.Left() || xPos > bufferSize.RightInclusive())
{
return xPos;
}
COORD position{ xPos, yPos };
const auto attr = _buffer->GetCellDataAt(position)->DbcsAttr();
if (attr.IsTrailing())
{
// move off by highlighting the lead half too.
// alters position.X
bufferSize.DecrementInBounds(position);
}
return position.X;
}
// Method Description:
// - Expands the selection right-wards to cover a wide glyph, if necessary
// Arguments:
// - position: the (x,y) coordinate on the visible viewport
// Return Value:
// - updated x position to encapsulate the wide glyph
SHORT Terminal::_ExpandWideGlyphSelectionRight(const SHORT xPos, const SHORT yPos) const
{
// don't change the value if at/outside the boundary
const auto bufferSize = _buffer->GetSize();
if (xPos < bufferSize.Left() || xPos >= bufferSize.RightInclusive())
{
return xPos;
}
COORD position{ xPos, yPos };
const auto attr = _buffer->GetCellDataAt(position)->DbcsAttr();
if (attr.IsLeading())
{
// move off by highlighting the trailing half too.
// alters position.X
bufferSize.IncrementInBounds(position);
}
return position.X;
return _selection->end;
}
// Method Description:
@ -219,7 +88,7 @@ SHORT Terminal::_ExpandWideGlyphSelectionRight(const SHORT xPos, const SHORT yPo
// - bool representing if selection is only a single cell. Used for copyOnSelect
const bool Terminal::_IsSingleCellSelection() const noexcept
{
return (_selectionAnchor == _endSelectionPosition);
return (_selection->start == _selection->end);
}
// Method Description:
@ -234,7 +103,7 @@ const bool Terminal::IsSelectionActive() const noexcept
{
return false;
}
return _selectionActive;
return _selection.has_value();
}
// Method Description:
@ -247,79 +116,60 @@ const bool Terminal::IsCopyOnSelectActive() const noexcept
}
// Method Description:
// - Select the sequence between delimiters defined in Settings
// - Perform a multi-click selection at viewportPos expanding according to the expansionMode
// Arguments:
// - position: the (x,y) coordinate on the visible viewport
void Terminal::DoubleClickSelection(const COORD position)
// - viewportPos: the (x,y) coordinate on the visible viewport
// - expansionMode: the SelectionExpansionMode to dictate the boundaries of the selection anchors
void Terminal::MultiClickSelection(const COORD viewportPos, SelectionExpansionMode expansionMode)
{
#pragma warning(suppress : 26496) // cpp core checks wants this const but .Clamp() can write it.
COORD positionWithOffsets = _ConvertToBufferCell(position);
// set the selection pivot to expand the selection using SetSelectionEnd()
_selection = SelectionAnchors{};
_selection->pivot = _ConvertToBufferCell(viewportPos);
// scan leftwards until delimiter is found and
// set selection anchor to one right of that spot
_selectionAnchor = _ExpandDoubleClickSelectionLeft(positionWithOffsets);
THROW_IF_FAILED(ShortSub(_selectionAnchor.Y, gsl::narrow<SHORT>(ViewStartIndex()), &_selectionAnchor.Y));
_selectionVerticalOffset = gsl::narrow<SHORT>(ViewStartIndex());
_multiClickSelectionMode = expansionMode;
SetSelectionEnd(viewportPos);
// scan rightwards until delimiter is found and
// set endSelectionPosition to one left of that spot
_endSelectionPosition = _ExpandDoubleClickSelectionRight(positionWithOffsets);
THROW_IF_FAILED(ShortSub(_endSelectionPosition.Y, gsl::narrow<SHORT>(ViewStartIndex()), &_endSelectionPosition.Y));
_selectionActive = true;
_multiClickSelectionMode = SelectionExpansionMode::Word;
}
// Method Description:
// - Select the entire row of the position clicked
// Arguments:
// - position: the (x,y) coordinate on the visible viewport
void Terminal::TripleClickSelection(const COORD position)
{
SetSelectionAnchor({ 0, position.Y });
SetEndSelectionPosition({ _buffer->GetSize().RightInclusive(), position.Y });
_multiClickSelectionMode = SelectionExpansionMode::Line;
// we need to set the _selectionPivot again
// for future shift+clicks
_selection->pivot = _selection->start;
}
// Method Description:
// - Record the position of the beginning of a selection
// Arguments:
// - position: the (x,y) coordinate on the visible viewport
void Terminal::SetSelectionAnchor(const COORD position)
void Terminal::SetSelectionAnchor(const COORD viewportPos)
{
_selectionAnchor = position;
_selection = SelectionAnchors{};
_selection->pivot = _ConvertToBufferCell(viewportPos);
// include _scrollOffset here to ensure this maps to the right spot of the original viewport
THROW_IF_FAILED(ShortSub(_selectionAnchor.Y, gsl::narrow<SHORT>(_scrollOffset), &_selectionAnchor.Y));
// copy value of ViewStartIndex to support scrolling
// and update on new buffer output (used in _GetSelectionRects())
_selectionVerticalOffset = gsl::narrow<SHORT>(ViewStartIndex());
_selectionActive = true;
_allowSingleCharSelection = (_copyOnSelect) ? false : true;
SetEndSelectionPosition(position);
_multiClickSelectionMode = SelectionExpansionMode::Cell;
SetSelectionEnd(viewportPos);
_selection->start = _selection->pivot;
}
// Method Description:
// - Record the position of the end of a selection
// - Update selection anchors when dragging to a position
// - based on the selection expansion mode
// Arguments:
// - position: the (x,y) coordinate on the visible viewport
void Terminal::SetEndSelectionPosition(const COORD position)
// - viewportPos: the (x,y) coordinate on the visible viewport
// - newExpansionMode: overwrites the _multiClickSelectionMode for this function call. Used for ShiftClick
void Terminal::SetSelectionEnd(const COORD viewportPos, std::optional<SelectionExpansionMode> newExpansionMode)
{
_endSelectionPosition = position;
const auto textBufferPos = _ConvertToBufferCell(viewportPos);
// include _scrollOffset here to ensure this maps to the right spot of the original viewport
THROW_IF_FAILED(ShortSub(_endSelectionPosition.Y, gsl::narrow<SHORT>(_scrollOffset), &_endSelectionPosition.Y));
// if this is a shiftClick action, we need to overwrite the _multiClickSelectionMode value (even if it's the same)
// Otherwise, we may accidentally expand during other selection-based actions
_multiClickSelectionMode = newExpansionMode.has_value() ? *newExpansionMode : _multiClickSelectionMode;
// copy value of ViewStartIndex to support scrolling
// and update on new buffer output (used in _GetSelectionRects())
_selectionVerticalOffset = gsl::narrow<SHORT>(ViewStartIndex());
const auto anchors = _PivotSelection(textBufferPos);
std::tie(_selection->start, _selection->end) = _ExpandSelectionAnchors(anchors);
// moving the endpoint of what used to be a single cell selection
// allows the user to drag back and select just one cell
if (_copyOnSelect && !_IsSingleCellSelection())
{
_allowSingleCharSelection = true;
@ -327,12 +177,65 @@ void Terminal::SetEndSelectionPosition(const COORD position)
}
// Method Description:
// - enable/disable box selection (ALT + selection)
// - returns a new pair of selection anchors for selecting around the pivot
// - This ensures start < end when compared
// Arguments:
// - isEnabled: new value for _boxSelection
void Terminal::SetBoxSelection(const bool isEnabled) noexcept
// - targetPos: the (x,y) coordinate we are moving to on the text buffer
// Return Value:
// - the new start/end for a selection
std::pair<COORD, COORD> Terminal::_PivotSelection(const COORD targetPos) const
{
_boxSelection = isEnabled;
if (_buffer->GetSize().CompareInBounds(targetPos, _selection->pivot) <= 0)
{
// target is before pivot
// treat target as start
return std::make_pair(targetPos, _selection->pivot);
}
else
{
// target is after pivot
// treat pivot as start
return std::make_pair(_selection->pivot, targetPos);
}
}
// Method Description:
// - Update the selection anchors to expand according to the expansion mode
// Arguments:
// - anchors: a pair of selection anchors representing a desired selection
// Return Value:
// - the new start/end for a selection
std::pair<COORD, COORD> Terminal::_ExpandSelectionAnchors(std::pair<COORD, COORD> anchors) const
{
COORD start = anchors.first;
COORD end = anchors.second;
const auto bufferSize = _buffer->GetSize();
switch (_multiClickSelectionMode)
{
case SelectionExpansionMode::Line:
start = { bufferSize.Left(), start.Y };
end = { bufferSize.RightInclusive(), end.Y };
break;
case SelectionExpansionMode::Word:
start = _buffer->GetWordStart(start, _wordDelimiters);
end = _buffer->GetWordEnd(end, _wordDelimiters);
break;
case SelectionExpansionMode::Cell:
default:
// no expansion is necessary
break;
}
return std::make_pair(start, end);
}
// Method Description:
// - enable/disable block selection (ALT + selection)
// Arguments:
// - isEnabled: new value for _blockSelection
void Terminal::SetBlockSelection(const bool isEnabled) noexcept
{
_blockSelection = isEnabled;
}
// Method Description:
@ -340,11 +243,8 @@ void Terminal::SetBoxSelection(const bool isEnabled) noexcept
#pragma warning(disable : 26440) // changing this to noexcept would require a change to ConHost's selection model
void Terminal::ClearSelection()
{
_selectionActive = false;
_allowSingleCharSelection = false;
_selectionAnchor = { 0, 0 };
_endSelectionPosition = { 0, 0 };
_selectionVerticalOffset = 0;
_selection = std::nullopt;
}
// Method Description:
@ -359,47 +259,13 @@ const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool tri
std::function<COLORREF(TextAttribute&)> GetForegroundColor = std::bind(&Terminal::GetForegroundColor, this, std::placeholders::_1);
std::function<COLORREF(TextAttribute&)> GetBackgroundColor = std::bind(&Terminal::GetBackgroundColor, this, std::placeholders::_1);
return _buffer->GetTextForClipboard(!_boxSelection,
return _buffer->GetTextForClipboard(!_blockSelection,
trimTrailingWhitespace,
_GetSelectionRects(),
GetForegroundColor,
GetBackgroundColor);
}
// Method Description:
// - expand the double click selection to the left
// - stopped by delimiter if started on delimiter
// Arguments:
// - position: buffer coordinate for selection
// Return Value:
// - updated copy of "position" to new expanded location (with vertical offset)
COORD Terminal::_ExpandDoubleClickSelectionLeft(const COORD position) const
{
// force position to be within bounds
#pragma warning(suppress : 26496) // cpp core checks wants this const but .Clamp() can write it.
COORD positionWithOffsets = position;
_buffer->GetSize().Clamp(positionWithOffsets);
return _buffer->GetWordStart(positionWithOffsets, _wordDelimiters);
}
// Method Description:
// - expand the double click selection to the right
// - stopped by delimiter if started on delimiter
// Arguments:
// - position: buffer coordinate for selection
// Return Value:
// - updated copy of "position" to new expanded location (with vertical offset)
COORD Terminal::_ExpandDoubleClickSelectionRight(const COORD position) const
{
// force position to be within bounds
#pragma warning(suppress : 26496) // cpp core checks wants this const but .Clamp() can write it.
COORD positionWithOffsets = position;
_buffer->GetSize().Clamp(positionWithOffsets);
return _buffer->GetWordEnd(positionWithOffsets, _wordDelimiters);
}
// Method Description:
// - convert viewport position to the corresponding location on the buffer
// Arguments:
@ -408,13 +274,10 @@ COORD Terminal::_ExpandDoubleClickSelectionRight(const COORD position) const
// - the corresponding location on the buffer
COORD Terminal::_ConvertToBufferCell(const COORD viewportPos) const
{
// Force position to be valid
COORD positionWithOffsets = viewportPos;
_buffer->GetSize().Clamp(positionWithOffsets);
THROW_IF_FAILED(ShortSub(viewportPos.Y, gsl::narrow<SHORT>(_scrollOffset), &positionWithOffsets.Y));
THROW_IF_FAILED(ShortAdd(positionWithOffsets.Y, gsl::narrow<SHORT>(ViewStartIndex()), &positionWithOffsets.Y));
return positionWithOffsets;
const auto yPos = base::ClampedNumeric<short>(_VisibleStartIndex()) + viewportPos.Y;
COORD bufferPos = { viewportPos.X, yPos };
_buffer->GetSize().Clamp(bufferPos);
return bufferPos;
}
// Method Description:

View file

@ -173,7 +173,7 @@ void Terminal::SelectNewRegion(const COORD coordStart, const COORD coordEnd)
realCoordEnd.Y -= gsl::narrow<short>(_VisibleStartIndex());
SetSelectionAnchor(realCoordStart);
SetEndSelectionPosition(realCoordEnd);
SetSelectionEnd(realCoordEnd, SelectionExpansionMode::Cell);
}
const std::wstring Terminal::GetConsoleTitle() const noexcept

View file

@ -72,7 +72,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionAnchor({ 5, rowValue });
// Simulate move to (x,y) = (15,20)
term.SetEndSelectionPosition({ 15, 20 });
term.SetSelectionEnd({ 15, 20 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
@ -110,19 +110,19 @@ namespace TerminalCoreUnitTests
{
const COORD maxCoord = { SHRT_MAX, SHRT_MAX };
// Test SetSelectionAnchor(COORD) and SetEndSelectionPosition(COORD)
// Test SetSelectionAnchor(COORD) and SetSelectionEnd(COORD)
// Behavior: clamp coord to viewport.
auto ValidateSingleClickSelection = [&](SHORT scrollback, SMALL_RECT expected) {
Terminal term;
DummyRenderTarget emptyRT;
term.Create({ 10, 10 }, scrollback, emptyRT);
// NOTE: SetEndSelectionPosition(COORD) is called within SetSelectionAnchor(COORD)
// NOTE: SetSelectionEnd(COORD) is called within SetSelectionAnchor(COORD)
term.SetSelectionAnchor(maxCoord);
ValidateSingleRowSelection(term, expected);
};
// Test DoubleClickSelection(COORD)
// Test a Double Click Selection
// Behavior: clamp coord to viewport.
// Then, do double click selection.
auto ValidateDoubleClickSelection = [&](SHORT scrollback, SMALL_RECT expected) {
@ -130,11 +130,11 @@ namespace TerminalCoreUnitTests
DummyRenderTarget emptyRT;
term.Create({ 10, 10 }, scrollback, emptyRT);
term.DoubleClickSelection(maxCoord);
term.MultiClickSelection(maxCoord, Terminal::SelectionExpansionMode::Word);
ValidateSingleRowSelection(term, expected);
};
// Test TripleClickSelection(COORD)
// Test a Triple Click Selection
// Behavior: clamp coord to viewport.
// Then, do triple click selection.
auto ValidateTripleClickSelection = [&](SHORT scrollback, SMALL_RECT expected) {
@ -142,7 +142,7 @@ namespace TerminalCoreUnitTests
DummyRenderTarget emptyRT;
term.Create({ 10, 10 }, scrollback, emptyRT);
term.TripleClickSelection(maxCoord);
term.MultiClickSelection(maxCoord, Terminal::SelectionExpansionMode::Line);
ValidateSingleRowSelection(term, expected);
};
@ -226,17 +226,17 @@ namespace TerminalCoreUnitTests
// Case 1: Move out of right boundary
Log::Comment(L"Out of bounds: X-value too large");
term.SetEndSelectionPosition({ 20, 5 });
term.SetSelectionEnd({ 20, 5 });
ValidateSingleRowSelection(term, SMALL_RECT({ 5, 5, rightBoundary, 5 }));
// Case 2: Move out of left boundary
Log::Comment(L"Out of bounds: X-value negative");
term.SetEndSelectionPosition({ -20, 5 });
term.SetSelectionEnd({ -20, 5 });
ValidateSingleRowSelection(term, { leftBoundary, 5, 5, 5 });
// Case 3: Move out of top boundary
Log::Comment(L"Out of bounds: Y-value negative");
term.SetEndSelectionPosition({ 5, -20 });
term.SetSelectionEnd({ 5, -20 });
{
auto selectionRects = term.GetSelectionRects();
@ -267,7 +267,7 @@ namespace TerminalCoreUnitTests
// Case 4: Move out of bottom boundary
Log::Comment(L"Out of bounds: Y-value too large");
term.SetEndSelectionPosition({ 5, 20 });
term.SetSelectionEnd({ 5, 20 });
{
auto selectionRects = term.GetSelectionRects();
@ -310,10 +310,10 @@ namespace TerminalCoreUnitTests
// Simulate ALT + click at (x,y) = (5,10)
term.SetSelectionAnchor({ 5, rowValue });
term.SetBoxSelection(true);
term.SetBlockSelection(true);
// Simulate move to (x,y) = (15,20)
term.SetEndSelectionPosition({ 15, 20 });
term.SetSelectionEnd({ 15, 20 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
@ -349,7 +349,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionAnchor({ 5, rowValue });
// Simulate move to (x,y) = (15,20)
term.SetEndSelectionPosition({ 15, 20 });
term.SetSelectionEnd({ 15, 20 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
@ -449,10 +449,10 @@ namespace TerminalCoreUnitTests
// Simulate ALT + click at (x,y) = (5,8)
term.SetSelectionAnchor({ 5, 8 });
term.SetBoxSelection(true);
term.SetBlockSelection(true);
// Simulate move to (x,y) = (7,12)
term.SetEndSelectionPosition({ 7, 12 });
term.SetSelectionEnd({ 7, 12 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
@ -501,7 +501,7 @@ namespace TerminalCoreUnitTests
// Simulate double click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.DoubleClickSelection(clickPos);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word);
// Validate selection area
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, (4 + gsl::narrow<SHORT>(text.size()) - 1), 10 }));
@ -519,7 +519,7 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.DoubleClickSelection(clickPos);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word);
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
@ -546,7 +546,7 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (15,10)
// this is over the '>' char
auto clickPos = COORD{ 15, 10 };
term.DoubleClickSelection(clickPos);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Word);
// ---Validate selection area---
// "Terminal" is in class 2
@ -572,14 +572,14 @@ namespace TerminalCoreUnitTests
term.Write(text);
// Simulate double click at (x,y) = (5,10)
term.DoubleClickSelection({ 5, 10 });
term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansionMode::Word);
// Simulate move to (x,y) = (21,10)
//
// buffer: doubleClickMe dragThroughHere
// ^ ^
// start finish
term.SetEndSelectionPosition({ 21, 10 });
term.SetSelectionEnd({ 21, 10 });
// Validate selection area
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 32, 10 }));
@ -601,14 +601,14 @@ namespace TerminalCoreUnitTests
term.Write(text);
// Simulate double click at (x,y) = (21,10)
term.DoubleClickSelection({ 21, 10 });
term.MultiClickSelection({ 21, 10 }, Terminal::SelectionExpansionMode::Word);
// Simulate move to (x,y) = (5,10)
//
// buffer: doubleClickMe dragThroughHere
// ^ ^
// finish start
term.SetEndSelectionPosition({ 5, 10 });
term.SetSelectionEnd({ 5, 10 });
// Validate selection area
ValidateSingleRowSelection(term, SMALL_RECT({ 4, 10, 32, 10 }));
@ -622,7 +622,7 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.TripleClickSelection(clickPos);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line);
// Validate selection area
ValidateSingleRowSelection(term, SMALL_RECT({ 0, 10, 99, 10 }));
@ -636,10 +636,10 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.TripleClickSelection(clickPos);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line);
// Simulate move to (x,y) = (7,10)
term.SetEndSelectionPosition({ 7, 10 });
term.SetSelectionEnd({ 7, 10 });
// Validate selection area
ValidateSingleRowSelection(term, SMALL_RECT({ 0, 10, 99, 10 }));
@ -653,10 +653,10 @@ namespace TerminalCoreUnitTests
// Simulate click at (x,y) = (5,10)
auto clickPos = COORD{ 5, 10 };
term.TripleClickSelection(clickPos);
term.MultiClickSelection(clickPos, Terminal::SelectionExpansionMode::Line);
// Simulate move to (x,y) = (5,11)
term.SetEndSelectionPosition({ 5, 11 });
term.SetSelectionEnd({ 5, 11 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
@ -689,7 +689,7 @@ namespace TerminalCoreUnitTests
// Simulate move to (x,y) = (5,10)
// (So, no movement)
term.SetEndSelectionPosition({ 5, 10 });
term.SetSelectionEnd({ 5, 10 });
// Case 1: single cell selection not allowed
{
@ -705,12 +705,12 @@ namespace TerminalCoreUnitTests
}
// Case 2: move off of single cell
term.SetEndSelectionPosition({ 6, 10 });
term.SetSelectionEnd({ 6, 10 });
ValidateSingleRowSelection(term, { 5, 10, 6, 10 });
VERIFY_IS_TRUE(term.IsSelectionActive());
// Case 3: move back onto single cell (now allowed)
term.SetEndSelectionPosition({ 5, 10 });
term.SetSelectionEnd({ 5, 10 });
ValidateSingleRowSelection(term, { 5, 10, 5, 10 });
// single cell selection should now be allowed

View file

@ -400,7 +400,7 @@ const COORD RenderData::GetSelectionAnchor() const noexcept
// - none
// Return Value:
// - current selection anchor
const COORD RenderData::GetEndSelectionPosition() const noexcept
const COORD RenderData::GetSelectionEnd() const noexcept
{
// The selection area in ConHost is encoded as two things...
// - SelectionAnchor: the initial position where the selection was started

View file

@ -61,7 +61,7 @@ public:
void ClearSelection() override;
void SelectNewRegion(const COORD coordStart, const COORD coordEnd) override;
const COORD GetSelectionAnchor() const noexcept;
const COORD GetEndSelectionPosition() const noexcept;
const COORD GetSelectionEnd() const noexcept;
void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr);
#pragma endregion
};

View file

@ -34,111 +34,6 @@ Selection& Selection::Instance()
return *_instance;
}
// Routine Description:
// - Determines the line-by-line selection rectangles based on global selection state.
// Arguments:
// - selectionRect - The selection rectangle outlining the region to be selected
// - selectionAnchor - The corner of the selection rectangle that selection started from
// - lineSelection - True to process in line mode. False to process in block mode.
// Return Value:
// - Returns a vector where each SMALL_RECT is one Row worth of the area to be selected.
// - Returns empty vector if no rows are selected.
// - Throws exceptions for out of memory issues
std::vector<SMALL_RECT> Selection::s_GetSelectionRects(const SMALL_RECT& selectionRect,
const COORD selectionAnchor,
const bool lineSelection)
{
std::vector<SMALL_RECT> selectionAreas;
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto& screenInfo = gci.GetActiveOutputBuffer();
// if the anchor (start of select) was in the top right or bottom left of the box,
// we need to remove rectangular overlap in the middle.
// e.g.
// For selections with the anchor in the top left (A) or bottom right (B),
// it is valid to maintain the inner rectangle (+) as part of the selection
// A+++++++================
// ==============++++++++B
// + and = are valid highlights in this scenario.
// For selections with the anchor in in the top right (A) or bottom left (B),
// we must remove a portion of the first/last line that lies within the rectangle (+)
// +++++++A=================
// ==============B+++++++
// Only = is valid for highlight in this scenario.
// This is only needed for line selection. Box selection doesn't need to account for this.
bool removeRectPortion = false;
if (lineSelection)
{
const auto selectionStart = selectionAnchor;
// only if top and bottom aren't the same line... we need the whole rectangle if we're on the same line.
// e.g. A++++++++++++++B
// All the + are valid select points.
if (selectionRect.Top != selectionRect.Bottom)
{
if ((selectionStart.X == selectionRect.Right && selectionStart.Y == selectionRect.Top) ||
(selectionStart.X == selectionRect.Left && selectionStart.Y == selectionRect.Bottom))
{
removeRectPortion = true;
}
}
}
// for each row within the selection rectangle
for (short i = selectionRect.Top; i <= selectionRect.Bottom; i++)
{
// create a rectangle representing the highlight on one row
SMALL_RECT highlightRow;
highlightRow.Top = i;
highlightRow.Bottom = i;
highlightRow.Left = selectionRect.Left;
highlightRow.Right = selectionRect.Right;
// compensate for line selection by extending one or both ends of the rectangle to the edge
if (lineSelection)
{
// if not the first row, pad the left selection to the buffer edge
if (i != selectionRect.Top)
{
highlightRow.Left = 0;
}
// if not the last row, pad the right selection to the buffer edge
if (i != selectionRect.Bottom)
{
highlightRow.Right = screenInfo.GetBufferSize().RightInclusive();
}
// if we've determined we're in a scenario where we must remove the inner rectangle from the lines...
if (removeRectPortion)
{
if (i == selectionRect.Top)
{
// from the top row, move the left edge of the highlight line to the right edge of the rectangle
highlightRow.Left = selectionRect.Right;
}
else if (i == selectionRect.Bottom)
{
// from the bottom row, move the right edge of the highlight line to the left edge of the rectangle
highlightRow.Right = selectionRect.Left;
}
}
}
// compensate for double width characters by calling double-width measuring/limiting function
const COORD targetPoint{ highlightRow.Left, highlightRow.Top };
const SHORT stringLength = highlightRow.Right - highlightRow.Left + 1;
highlightRow = s_BisectSelection(stringLength, targetPoint, screenInfo, highlightRow);
selectionAreas.emplace_back(highlightRow);
}
return selectionAreas;
}
// Routine Description:
// - Determines the line-by-line selection rectangles based on global selection state.
// Arguments:
@ -154,65 +49,17 @@ std::vector<SMALL_RECT> Selection::GetSelectionRects() const
return std::vector<SMALL_RECT>();
}
return s_GetSelectionRects(_srSelectionRect, _coordSelectionAnchor, IsLineSelection());
}
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto& screenInfo = gci.GetActiveOutputBuffer();
// Routine Description:
// - This routine checks to ensure that clipboard selection isn't trying to cut a double byte character in half.
// It will adjust the SmallRect rectangle size to ensure this.
// Arguments:
// - sStringLength - The length of the string we're attempting to clip.
// - coordTargetPoint - The row/column position within the text buffer that we're about to try to clip.
// - screenInfo - Screen information structure containing relevant text and dimension information.
// - rect - The region of the text that we want to clip, and then adjusted to the region that should be
// clipped without splicing double-width characters.
// Return Value:
// - the clipped region
SMALL_RECT Selection::s_BisectSelection(const short sStringLength,
const COORD coordTargetPoint,
const SCREEN_INFORMATION& screenInfo,
const SMALL_RECT rect)
{
SMALL_RECT outRect = rect;
try
{
auto iter = screenInfo.GetCellDataAt(coordTargetPoint);
if (iter->DbcsAttr().IsTrailing())
{
if (coordTargetPoint.X == 0)
{
outRect.Left++;
}
else
{
outRect.Left--;
}
}
// _coordSelectionAnchor is at one of the corners of _srSelectionRects
// endSelectionAnchor is at the exact opposite corner
COORD endSelectionAnchor;
endSelectionAnchor.X = (_coordSelectionAnchor.X == _srSelectionRect.Left) ? _srSelectionRect.Right : _srSelectionRect.Left;
endSelectionAnchor.Y = (_coordSelectionAnchor.Y == _srSelectionRect.Top) ? _srSelectionRect.Bottom : _srSelectionRect.Top;
// Check end position of strings
if (coordTargetPoint.X + sStringLength < screenInfo.GetBufferSize().Width())
{
iter += sStringLength;
if (iter->DbcsAttr().IsTrailing())
{
outRect.Right++;
}
}
else
{
if (coordTargetPoint.Y + 1 < screenInfo.GetBufferSize().Height())
{
const auto nextLineIter = screenInfo.GetCellDataAt({ 0, coordTargetPoint.Y + 1 });
if (nextLineIter->DbcsAttr().IsTrailing())
{
outRect.Right--;
}
}
}
}
CATCH_LOG();
return outRect;
const auto blockSelection = !IsLineSelection();
return screenInfo.GetTextBuffer().GetTextRects(_coordSelectionAnchor, endSelectionAnchor, blockSelection);
}
// Routine Description:
@ -564,18 +411,13 @@ void Selection::ColorSelection(const SMALL_RECT& srRect, const TextAttribute att
// - attr - Color to apply to region.
void Selection::ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr)
{
// Make a rectangle for the region as if it were selected by a mouse.
// We will use the first one as the "anchor" to represent where the mouse went down.
SMALL_RECT srSelection;
srSelection.Top = std::min(coordSelectionStart.Y, coordSelectionEnd.Y);
srSelection.Bottom = std::max(coordSelectionStart.Y, coordSelectionEnd.Y);
srSelection.Left = std::min(coordSelectionStart.X, coordSelectionEnd.X);
srSelection.Right = std::max(coordSelectionStart.X, coordSelectionEnd.X);
// Extract row-by-row selection rectangles for the selection area.
try
{
const auto rectangles = s_GetSelectionRects(srSelection, coordSelectionStart, true);
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto& screenInfo = gci.GetActiveOutputBuffer();
const auto rectangles = screenInfo.GetTextBuffer().GetTextRects(coordSelectionStart, coordSelectionEnd);
for (const auto& rect : rectangles)
{
ColorSelection(rect, attr);

View file

@ -72,15 +72,6 @@ private:
void _PaintSelection() const;
static SMALL_RECT s_BisectSelection(const short sStringLength,
const COORD coordTargetPoint,
const SCREEN_INFORMATION& screenInfo,
const SMALL_RECT rect);
static std::vector<SMALL_RECT> s_GetSelectionRects(const SMALL_RECT& selectionRect,
const COORD selectionAnchor,
const bool lineSelection);
void _CancelMarkSelection();
void _CancelMouseSelection();

View file

@ -323,7 +323,7 @@ class SelectionTests
// selection rectangle starts from the target and goes for the length requested
srSelection.Left = coordTargetPoint.X;
srSelection.Right = coordTargetPoint.X + sStringLength - 1;
srSelection.Right = coordTargetPoint.X + sStringLength;
// save original for comparison
srOriginal.Top = srSelection.Top;
@ -331,7 +331,12 @@ class SelectionTests
srOriginal.Left = srSelection.Left;
srOriginal.Right = srSelection.Right;
srSelection = Selection::s_BisectSelection(sStringLength, coordTargetPoint, screenInfo, srSelection);
COORD startPos{ sTargetX, sTargetY };
COORD endPos{ base::ClampAdd(sTargetX, sLength), sTargetY };
const auto selectionRects = screenInfo.GetTextBuffer().GetTextRects(startPos, endPos);
VERIFY_ARE_EQUAL(static_cast<size_t>(1), selectionRects.size());
srSelection = selectionRects.at(0);
VERIFY_ARE_EQUAL(srOriginal.Top, srSelection.Top);
VERIFY_ARE_EQUAL(srOriginal.Bottom, srSelection.Bottom);
@ -378,10 +383,10 @@ class SelectionTests
// start from position 10 before end of row (80 length row)
// row is 2
// selection is 10 characters long
// selection is 9 characters long
// the left edge shouldn't move
// the right edge should move one to the left (-1) to not select the leading byte
TestBisectSelectionDelta(70, 2, 10, 0, -1);
TestBisectSelectionDelta(70, 2, 9, 0, -1);
// 2b. End position is leading half and is elsewhere in the row
@ -389,16 +394,16 @@ class SelectionTests
// row is 2
// selection is 10 characters long
// the left edge shouldn't move
// the right edge should move one to the right (+1) to add the trailing byte to the selection
TestBisectSelectionDelta(58, 2, 10, 0, 1);
// the right edge should not move, because it is already on the trailing byte
TestBisectSelectionDelta(58, 2, 10, 0, 0);
// 2c. End position is leading half and is at end of buffer
// start from position 10 before end of row (80 length row)
// row is 300 (or 299 for the index)
// selection is 10 characters long
// selection is 9 characters long
// the left edge shouldn't move
// the right edge shouldn't move
TestBisectSelectionDelta(70, 299, 10, 0, 0);
// the right edge should move one to the left (-1) to not select the leading byte
TestBisectSelectionDelta(70, 299, 9, 0, -1);
}
};

View file

@ -148,6 +148,8 @@ class TextBufferTests
void WriteLinesToBuffer(const std::vector<std::wstring>& text, TextBuffer& buffer);
TEST_METHOD(GetWordBoundaries);
TEST_METHOD(GetTextRects);
};
void TextBufferTests::TestBufferCreate()
@ -2136,3 +2138,68 @@ void TextBufferTests::GetWordBoundaries()
VERIFY_ARE_EQUAL(expected, result);
}
}
void TextBufferTests::GetTextRects()
{
// GetTextRects() is used to...
// - Represent selection rects
// - Represent UiaTextRanges for accessibility
// This is the burrito emoji: 🌯
// It's encoded in UTF-16, as needed by the buffer.
const auto burrito = std::wstring(L"\xD83C\xDF2F");
COORD bufferSize{ 20, 50 };
UINT cursorSize = 12;
TextAttribute attr{ 0x7f };
auto _buffer = std::make_unique<TextBuffer>(bufferSize, attr, cursorSize, _renderTarget);
// Setup: Write lines of text to the buffer
const std::vector<std::wstring> text = { L"0123456789",
L" " + burrito + L"3456" + burrito,
L" " + burrito + L"45" + burrito,
burrito + L"234567" + burrito,
L"0123456789" };
WriteLinesToBuffer(text, *_buffer);
// - - - Text Buffer Contents - - -
// |0123456789
// | 🌯3456🌯
// | 🌯45🌯
// |🌯234567🌯
// |0123456789
// - - - - - - - - - - - - - - - -
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:blockSelection", L"{false, true}")
END_TEST_METHOD_PROPERTIES();
bool blockSelection;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"blockSelection", blockSelection), L"Get 'blockSelection' variant");
std::vector<SMALL_RECT> expected{};
if (blockSelection)
{
expected.push_back({ 1, 0, 7, 0 });
expected.push_back({ 1, 1, 8, 1 }); // expand right
expected.push_back({ 1, 2, 7, 2 });
expected.push_back({ 0, 3, 7, 3 }); // expand left
expected.push_back({ 1, 4, 7, 4 });
}
else
{
expected.push_back({ 1, 0, 19, 0 });
expected.push_back({ 0, 1, 19, 1 });
expected.push_back({ 0, 2, 19, 2 });
expected.push_back({ 0, 3, 19, 3 });
expected.push_back({ 0, 4, 7, 4 });
}
COORD start{ 1, 0 };
COORD end{ 7, 4 };
const auto result = _buffer->GetTextRects(start, end, blockSelection);
VERIFY_ARE_EQUAL(expected.size(), result.size());
for (size_t i = 0; i < expected.size(); ++i)
{
VERIFY_ARE_EQUAL(expected.at(i), result.at(i));
}
}

View file

@ -105,7 +105,7 @@ HRESULT ScreenInfoUiaProvider::GetSelectionRange(_In_ IRawElementProviderSimple*
const auto start = _pData->GetSelectionAnchor();
// we need to make end exclusive
auto end = _pData->GetEndSelectionPosition();
auto end = _pData->GetSelectionEnd();
_pData->GetTextBuffer().GetSize().IncrementInBounds(end, true);
// TODO GH #4509: Box Selection is misrepresented here as a line selection.

View file

@ -37,7 +37,7 @@ namespace Microsoft::Console::Types
virtual void ClearSelection() = 0;
virtual void SelectNewRegion(const COORD coordStart, const COORD coordEnd) = 0;
virtual const COORD GetSelectionAnchor() const noexcept = 0;
virtual const COORD GetEndSelectionPosition() const noexcept = 0;
virtual const COORD GetSelectionEnd() const noexcept = 0;
virtual void ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr) = 0;
};

View file

@ -125,7 +125,7 @@ HRESULT TermControlUiaProvider::GetSelectionRange(_In_ IRawElementProviderSimple
const auto start = _pData->GetSelectionAnchor();
// we need to make end exclusive
auto end = _pData->GetEndSelectionPosition();
auto end = _pData->GetSelectionEnd();
_pData->GetTextBuffer().GetSize().IncrementInBounds(end, true);
// TODO GH #4509: Box Selection is misrepresented here as a line selection.