From 0e672fac08d6316f3d69c9a1b2d9ab88aa4932ef Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Thu, 27 Feb 2020 16:42:26 -0800 Subject: [PATCH] Move rect expansion to textbuffer; refactor selection code (#4560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/buffer/out/textBuffer.cpp | 95 +++++ src/buffer/out/textBuffer.hpp | 4 + .../PublicTerminalCore/HwndTerminal.cpp | 4 +- src/cascadia/TerminalControl/TermControl.cpp | 12 +- src/cascadia/TerminalCore/Terminal.cpp | 8 +- src/cascadia/TerminalCore/Terminal.hpp | 43 +- .../TerminalCore/TerminalSelection.cpp | 399 ++++++------------ .../TerminalCore/terminalrenderdata.cpp | 2 +- .../UnitTests_TerminalCore/SelectionTest.cpp | 62 +-- src/host/renderData.cpp | 2 +- src/host/renderData.hpp | 2 +- src/host/selection.cpp | 184 +------- src/host/selection.hpp | 9 - src/host/ut_host/SelectionTests.cpp | 23 +- src/host/ut_host/TextBufferTests.cpp | 67 +++ .../win32/screenInfoUiaProvider.cpp | 2 +- src/types/IUiaData.h | 2 +- src/types/TermControlUiaProvider.cpp | 2 +- 18 files changed, 394 insertions(+), 528 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 68ff8d0b564..bff427d8fe4 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -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 TextBuffer::GetTextRects(COORD start, COORD end, bool blockSelection) const +{ + std::vector 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(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: diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 7400efb353a..3e870152d60 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -135,6 +135,8 @@ class TextBuffer final 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 GetTextRects(COORD start, COORD end, bool blockSelection = false) const; + class TextAndColor { public: @@ -193,6 +195,8 @@ class TextBuffer final ROW& _GetFirstRow(); ROW& _GetPrevRowNoWrap(const ROW& row); + void _ExpandTextRow(SMALL_RECT& selectionRow) const; + enum class DelimiterClass { ControlChar, diff --git a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp index 09d65c13fc1..8c574934104 100644 --- a/src/cascadia/PublicTerminalCore/HwndTerminal.cpp +++ b/src/cascadia/PublicTerminalCore/HwndTerminal.cpp @@ -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; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 72bb372c79f..42eeb355eab 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -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(terminalPosition.X, 0, lastVisibleCol); // save location (for rendering) + render - _terminal->SetEndSelectionPosition(terminalPosition); + _terminal->SetSelectionEnd(terminalPosition); _renderer->TriggerSelection(); } diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index e4acabbdc93..df55fafdaee 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -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(*this); auto engine = std::make_unique(std::move(dispatch)); diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 335e1ee469d..254e254e676 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -142,7 +142,7 @@ class Microsoft::Terminal::Core::Terminal final : 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 @@ class Microsoft::Terminal::Core::Terminal final : #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 newExpansionMode = std::nullopt); + void SetBlockSelection(const bool isEnabled) noexcept; const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace) const; #pragma endregion @@ -187,19 +192,20 @@ class Microsoft::Terminal::Core::Terminal final : 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 _selection; + bool _blockSelection; bool _allowSingleCharSelection; bool _copyOnSelect; - SHORT _selectionVerticalOffset; std::wstring _wordDelimiters; SelectionExpansionMode _multiClickSelectionMode; #pragma endregion @@ -248,15 +254,10 @@ class Microsoft::Terminal::Core::Terminal final : #pragma region TextSelection // These methods are defined in TerminalSelection.cpp std::vector _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 _PivotSelection(const COORD targetPos) const; + std::pair _ExpandSelectionAnchors(std::pair anchors) const; COORD _ConvertToBufferCell(const COORD viewportPos) const; const bool _IsSingleCellSelection() const noexcept; - std::tuple _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 diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index cdf229f9722..6e0aebaa0ab 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -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 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 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 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 -{ - 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 +const COORD Terminal::GetSelectionEnd() const noexcept { - // 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,92 +116,126 @@ 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(ViewStartIndex()), &_selectionAnchor.Y)); - _selectionVerticalOffset = gsl::narrow(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(ViewStartIndex()), &_endSelectionPosition.Y)); - - _selectionActive = true; - _multiClickSelectionMode = SelectionExpansionMode::Word; + // we need to set the _selectionPivot again + // for future shift+clicks + _selection->pivot = _selection->start; } // Method Description: -// - Select the entire row of the position clicked +// - Record the position of the beginning of a selection // Arguments: // - position: the (x,y) coordinate on the visible viewport -void Terminal::TripleClickSelection(const COORD position) +void Terminal::SetSelectionAnchor(const COORD viewportPos) { - SetSelectionAnchor({ 0, position.Y }); - SetEndSelectionPosition({ _buffer->GetSize().RightInclusive(), position.Y }); + _selection = SelectionAnchors{}; + _selection->pivot = _ConvertToBufferCell(viewportPos); - _multiClickSelectionMode = SelectionExpansionMode::Line; + _allowSingleCharSelection = (_copyOnSelect) ? false : true; + + _multiClickSelectionMode = SelectionExpansionMode::Cell; + SetSelectionEnd(viewportPos); + + _selection->start = _selection->pivot; } // Method Description: -// - Record the position of the beginning 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::SetSelectionAnchor(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 newExpansionMode) { - _selectionAnchor = 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(_selectionAnchor.Y, gsl::narrow(_scrollOffset), &_selectionAnchor.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(ViewStartIndex()); + const auto anchors = _PivotSelection(textBufferPos); + std::tie(_selection->start, _selection->end) = _ExpandSelectionAnchors(anchors); - _selectionActive = true; - _allowSingleCharSelection = (_copyOnSelect) ? false : true; - - SetEndSelectionPosition(position); - - _multiClickSelectionMode = SelectionExpansionMode::Cell; + // 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; + } } // Method Description: -// - Record the position of the end of a selection +// - returns a new pair of selection anchors for selecting around the pivot +// - This ensures start < end when compared // Arguments: -// - position: the (x,y) coordinate on the visible viewport -void Terminal::SetEndSelectionPosition(const COORD position) +// - targetPos: the (x,y) coordinate we are moving to on the text buffer +// Return Value: +// - the new start/end for a selection +std::pair Terminal::_PivotSelection(const COORD targetPos) const { - _endSelectionPosition = position; - - // include _scrollOffset here to ensure this maps to the right spot of the original viewport - THROW_IF_FAILED(ShortSub(_endSelectionPosition.Y, gsl::narrow(_scrollOffset), &_endSelectionPosition.Y)); + 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); + } +} - // copy value of ViewStartIndex to support scrolling - // and update on new buffer output (used in _GetSelectionRects()) - _selectionVerticalOffset = gsl::narrow(ViewStartIndex()); +// 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 Terminal::_ExpandSelectionAnchors(std::pair anchors) const +{ + COORD start = anchors.first; + COORD end = anchors.second; - if (_copyOnSelect && !_IsSingleCellSelection()) + const auto bufferSize = _buffer->GetSize(); + switch (_multiClickSelectionMode) { - _allowSingleCharSelection = true; + 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 box selection (ALT + selection) +// - enable/disable block selection (ALT + selection) // Arguments: -// - isEnabled: new value for _boxSelection -void Terminal::SetBoxSelection(const bool isEnabled) noexcept +// - isEnabled: new value for _blockSelection +void Terminal::SetBlockSelection(const bool isEnabled) noexcept { - _boxSelection = isEnabled; + _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 GetForegroundColor = std::bind(&Terminal::GetForegroundColor, this, std::placeholders::_1); std::function 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(_scrollOffset), &positionWithOffsets.Y)); - THROW_IF_FAILED(ShortAdd(positionWithOffsets.Y, gsl::narrow(ViewStartIndex()), &positionWithOffsets.Y)); - return positionWithOffsets; + const auto yPos = base::ClampedNumeric(_VisibleStartIndex()) + viewportPos.Y; + COORD bufferPos = { viewportPos.X, yPos }; + _buffer->GetSize().Clamp(bufferPos); + return bufferPos; } // Method Description: diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index 81794c4fba2..f894a54c968 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -173,7 +173,7 @@ void Terminal::SelectNewRegion(const COORD coordStart, const COORD coordEnd) realCoordEnd.Y -= gsl::narrow(_VisibleStartIndex()); SetSelectionAnchor(realCoordStart); - SetEndSelectionPosition(realCoordEnd); + SetSelectionEnd(realCoordEnd, SelectionExpansionMode::Cell); } const std::wstring Terminal::GetConsoleTitle() const noexcept diff --git a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp index 0c2e5df8136..447910e47eb 100644 --- a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp @@ -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(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 diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index f6e1451938e..5cd99ce185b 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -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 diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 3a548e0a812..6005412ddf3 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -61,7 +61,7 @@ class RenderData final : 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 }; diff --git a/src/host/selection.cpp b/src/host/selection.cpp index 1103cfd80b6..7da4ee0cf23 100644 --- a/src/host/selection.cpp +++ b/src/host/selection.cpp @@ -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 Selection::s_GetSelectionRects(const SMALL_RECT& selectionRect, - const COORD selectionAnchor, - const bool lineSelection) -{ - std::vector 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 Selection::GetSelectionRects() const return std::vector(); } - return s_GetSelectionRects(_srSelectionRect, _coordSelectionAnchor, IsLineSelection()); -} - -// 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--; - } - } + const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& screenInfo = gci.GetActiveOutputBuffer(); - // 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(); + // _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; - 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); diff --git a/src/host/selection.hpp b/src/host/selection.hpp index 76a241950da..8b51db52788 100644 --- a/src/host/selection.hpp +++ b/src/host/selection.hpp @@ -72,15 +72,6 @@ class Selection 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 s_GetSelectionRects(const SMALL_RECT& selectionRect, - const COORD selectionAnchor, - const bool lineSelection); - void _CancelMarkSelection(); void _CancelMouseSelection(); diff --git a/src/host/ut_host/SelectionTests.cpp b/src/host/ut_host/SelectionTests.cpp index 7a6aea220de..e332f3ad0cc 100644 --- a/src/host/ut_host/SelectionTests.cpp +++ b/src/host/ut_host/SelectionTests.cpp @@ -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(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); } }; diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 1722813292a..3343b716089 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -148,6 +148,8 @@ class TextBufferTests void WriteLinesToBuffer(const std::vector& 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(bufferSize, attr, cursorSize, _renderTarget); + + // Setup: Write lines of text to the buffer + const std::vector 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 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)); + } +} diff --git a/src/interactivity/win32/screenInfoUiaProvider.cpp b/src/interactivity/win32/screenInfoUiaProvider.cpp index 6aa03a32e63..873060c041e 100644 --- a/src/interactivity/win32/screenInfoUiaProvider.cpp +++ b/src/interactivity/win32/screenInfoUiaProvider.cpp @@ -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. diff --git a/src/types/IUiaData.h b/src/types/IUiaData.h index 3106cce532b..dda70459765 100644 --- a/src/types/IUiaData.h +++ b/src/types/IUiaData.h @@ -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; }; diff --git a/src/types/TermControlUiaProvider.cpp b/src/types/TermControlUiaProvider.cpp index 752029f78de..ae973a4fd70 100644 --- a/src/types/TermControlUiaProvider.cpp +++ b/src/types/TermControlUiaProvider.cpp @@ -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.