Introduce the concept of "selection spans" instead of "rects" (#17638)

This commit is contained in:
Dustin L. Howett 2024-08-15 14:00:40 -05:00 committed by GitHub
parent 65219d40ce
commit 1ef497970f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 297 additions and 554 deletions

View File

@ -208,7 +208,7 @@ public:
const std::vector<size_t> GetPatternId(const til::point location) const override;
std::pair<COLORREF, COLORREF> GetAttributeColors(const TextAttribute& attr) const noexcept override;
std::vector<Microsoft::Console::Types::Viewport> GetSelectionRects() noexcept override;
std::span<const til::point_span> GetSelectionSpans() const noexcept override;
std::span<const til::point_span> GetSearchHighlights() const noexcept override;
const til::point_span* GetSearchHighlightFocused() const noexcept override;
const bool IsSelectionActive() const noexcept override;
@ -356,6 +356,9 @@ private:
std::vector<til::point_span> _searchHighlights;
size_t _searchHighlightFocused = 0;
mutable std::vector<til::point_span> _lastSelectionSpans;
mutable til::generation_t _lastSelectionGeneration{};
CursorType _defaultCursorShape = CursorType::Legacy;
til::enumset<Mode> _systemMode{ Mode::AutoWrap };
@ -466,7 +469,6 @@ private:
#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
std::vector<til::inclusive_rect> _GetSelectionRects() const noexcept;
std::vector<til::point_span> _GetSelectionSpans() const noexcept;
std::pair<til::point, til::point> _PivotSelection(const til::point targetPos, bool& targetStart) const noexcept;
std::pair<til::point, til::point> _ExpandSelectionAnchors(std::pair<til::point, til::point> anchors) const;

View File

@ -42,27 +42,6 @@ DEFINE_ENUM_FLAG_OPERATORS(Terminal::SelectionEndpoint);
* 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:
// - A vector of rectangles representing the regions to select, line by line. They are absolute coordinates relative to the buffer origin.
std::vector<til::inclusive_rect> Terminal::_GetSelectionRects() const noexcept
{
std::vector<til::inclusive_rect> result;
if (!IsSelectionActive())
{
return result;
}
try
{
return _activeBuffer().GetTextRects(_selection->start, _selection->end, _selection->blockSelection, false);
}
CATCH_LOG();
return result;
}
// Method Description:
// - Identical to GetTextRects if it's a block selection, else returns a single span for the whole selection.
// Return Value:

View File

@ -127,17 +127,16 @@ std::pair<COLORREF, COLORREF> Terminal::GetAttributeColors(const TextAttribute&
return GetRenderSettings().GetAttributeColors(attr);
}
std::vector<Microsoft::Console::Types::Viewport> Terminal::GetSelectionRects() noexcept
std::span<const til::point_span> Terminal::GetSelectionSpans() const noexcept
try
{
std::vector<Viewport> result;
for (const auto& lineRect : _GetSelectionRects())
if (_selection.generation() != _lastSelectionGeneration)
{
result.emplace_back(Viewport::FromInclusive(lineRect));
_lastSelectionSpans = _GetSelectionSpans();
_lastSelectionGeneration = _selection.generation();
}
return result;
return _lastSelectionSpans;
}
catch (...)
{

View File

@ -336,7 +336,6 @@ namespace ControlUnitTests
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Drag the mouse down a whole row");
const til::point terminalPosition2{ 1, 1 };
@ -349,7 +348,6 @@ namespace ControlUnitTests
true);
Log::Comment(L"Verify that there's now two selections (one on each row)");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(2u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Release the mouse");
interactivity->PointerReleased(noMouseDown,
@ -358,7 +356,6 @@ namespace ControlUnitTests
cursorPosition2.to_core_point());
Log::Comment(L"Verify that there's still two selections");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(2u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"click outside the current selection");
const til::point terminalPosition3{ 2, 2 };
@ -370,7 +367,6 @@ namespace ControlUnitTests
cursorPosition3.to_core_point());
Log::Comment(L"Verify that there's now no selection");
VERIFY_IS_FALSE(core->HasSelection());
VERIFY_ARE_EQUAL(0u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Drag the mouse");
const til::point terminalPosition4{ 3, 2 };
@ -383,7 +379,6 @@ namespace ControlUnitTests
true);
Log::Comment(L"Verify that there's now one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
}
void ControlInteractivityTests::ScrollWithSelection()
@ -438,7 +433,6 @@ namespace ControlUnitTests
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Verify the location of the selection");
// The viewport is on row 21, so the selection will be on:
@ -586,7 +580,6 @@ namespace ControlUnitTests
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Verify that it started on the first cell we clicked on, not the one we dragged to");
til::point expectedAnchor{ 0, 0 };
@ -631,7 +624,6 @@ namespace ControlUnitTests
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Verify that it started on the first cell we clicked on, not the one we dragged to");
til::point expectedAnchor{ 0, 0 };
@ -801,7 +793,6 @@ namespace ControlUnitTests
true);
Log::Comment(L"Verify that there's one selection");
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
Log::Comment(L"Verify the location of the selection");
// The viewport is on row (historySize + 5), so the selection will be on:
@ -860,7 +851,6 @@ namespace ControlUnitTests
cursorPosition0.to_core_point(),
true);
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
{
const auto anchor{ core->_terminal->GetSelectionAnchor() };
const auto end{ core->_terminal->GetSelectionEnd() };
@ -885,7 +875,6 @@ namespace ControlUnitTests
cursorPosition1.to_core_point(),
true);
VERIFY_IS_TRUE(core->HasSelection());
VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size());
{
const auto anchor{ core->_terminal->GetSelectionAnchor() };
const auto end{ core->_terminal->GetSelectionEnd() };

View File

@ -38,7 +38,6 @@ namespace
HRESULT Invalidate(const til::rect* /*psrRegion*/) noexcept { return S_OK; }
HRESULT InvalidateCursor(const til::rect* /*psrRegion*/) noexcept { return S_OK; }
HRESULT InvalidateSystem(const til::rect* /*prcDirtyClient*/) noexcept { return S_OK; }
HRESULT InvalidateSelection(const std::vector<til::rect>& /*rectangles*/) noexcept { return S_OK; }
HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept
{
_triggerScrollDelta = *pcoordDelta;

View File

@ -32,16 +32,17 @@ namespace TerminalCoreUnitTests
// - expected: the expected value of the selection rect
// Return Value:
// - N/A
void ValidateSingleRowSelection(Terminal& term, const til::inclusive_rect& expected)
void ValidateLinearSelection(Terminal& term, const til::point start, const til::point end)
{
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
auto selectionSpans = term.GetSelectionSpans();
// Validate selection area
VERIFY_ARE_EQUAL(selectionRects.size(), static_cast<size_t>(1));
auto selection = term.GetViewport().ConvertToOrigin(selectionRects[0]).ToInclusive();
VERIFY_ARE_EQUAL(selectionSpans.size(), static_cast<size_t>(1));
VERIFY_ARE_EQUAL(selection, expected);
auto& sp{ selectionSpans[0] };
VERIFY_ARE_EQUAL(start, sp.start, L"start");
VERIFY_ARE_EQUAL(end, sp.end, L"end");
}
TextBuffer& GetTextBuffer(Terminal& term)
@ -59,7 +60,7 @@ namespace TerminalCoreUnitTests
auto clickPos = til::point{ 5, 10 };
term.SetSelectionAnchor(clickPos);
ValidateSingleRowSelection(term, { 5, 10, 5, 10 });
ValidateLinearSelection(term, { 5, 10 }, { 5, 10 });
}
TEST_METHOD(SelectArea)
@ -79,36 +80,8 @@ namespace TerminalCoreUnitTests
// Simulate move to (x,y) = (15,20)
term.SetSelectionEnd({ 15, 20 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
// Validate selection area
VERIFY_ARE_EQUAL(selectionRects.size(), static_cast<size_t>(11));
auto viewport = term.GetViewport();
auto rightBoundary = viewport.RightInclusive();
for (auto selectionRect : selectionRects)
{
auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive();
if (rowValue == 10)
{
// Verify top line
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 5, 10, rightBoundary, 10 }));
}
else if (rowValue == 20)
{
// Verify bottom line
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 0, 20, 15, 20 }));
}
else
{
// Verify other lines (full)
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 0, rowValue, rightBoundary, rowValue }));
}
rowValue++;
}
ValidateLinearSelection(term, { 5, rowValue }, { 15, 20 });
}
TEST_METHOD(OverflowTests)
@ -117,56 +90,56 @@ namespace TerminalCoreUnitTests
// Test SetSelectionAnchor(til::point) and SetSelectionEnd(til::point)
// Behavior: clamp coord to viewport.
auto ValidateSingleClickSelection = [&](til::CoordType scrollback, const til::inclusive_rect& expected) {
auto ValidateSingleClickSelection = [&](til::CoordType scrollback, const til::point start, const til::point end) {
Terminal term{ Terminal::TestDummyMarker{} };
DummyRenderer renderer{ &term };
term.Create({ 10, 10 }, scrollback, renderer);
// NOTE: SetSelectionEnd(til::point) is called within SetSelectionAnchor(til::point)
term.SetSelectionAnchor(maxCoord);
ValidateSingleRowSelection(term, expected);
ValidateLinearSelection(term, start, end);
};
// Test a Double Click Selection
// Behavior: clamp coord to viewport.
// Then, do double click selection.
auto ValidateDoubleClickSelection = [&](til::CoordType scrollback, const til::inclusive_rect& expected) {
auto ValidateDoubleClickSelection = [&](til::CoordType scrollback, const til::point start, const til::point end) {
Terminal term{ Terminal::TestDummyMarker{} };
DummyRenderer renderer{ &term };
term.Create({ 10, 10 }, scrollback, renderer);
term.MultiClickSelection(maxCoord, Terminal::SelectionExpansion::Word);
ValidateSingleRowSelection(term, expected);
ValidateLinearSelection(term, start, end);
};
// Test a Triple Click Selection
// Behavior: clamp coord to viewport.
// Then, do triple click selection.
auto ValidateTripleClickSelection = [&](til::CoordType scrollback, const til::inclusive_rect& expected) {
auto ValidateTripleClickSelection = [&](til::CoordType scrollback, const til::point start, const til::point end) {
Terminal term{ Terminal::TestDummyMarker{} };
DummyRenderer renderer{ &term };
term.Create({ 10, 10 }, scrollback, renderer);
term.MultiClickSelection(maxCoord, Terminal::SelectionExpansion::Line);
ValidateSingleRowSelection(term, expected);
ValidateLinearSelection(term, start, end);
};
// Test with no scrollback
Log::Comment(L"Single click selection with NO scrollback value");
ValidateSingleClickSelection(0, { 9, 9, 9, 9 });
ValidateSingleClickSelection(0, { 9, 9 }, { 9, 9 });
Log::Comment(L"Double click selection with NO scrollback value");
ValidateDoubleClickSelection(0, { 0, 9, 9, 9 });
ValidateDoubleClickSelection(0, { 0, 9 }, { 9, 9 });
Log::Comment(L"Triple click selection with NO scrollback value");
ValidateTripleClickSelection(0, { 0, 9, 9, 9 });
ValidateTripleClickSelection(0, { 0, 9 }, { 9, 9 });
// Test with max scrollback
const til::CoordType expected_row = SHRT_MAX - 1;
Log::Comment(L"Single click selection with MAXIMUM scrollback value");
ValidateSingleClickSelection(SHRT_MAX, { 9, expected_row, 9, expected_row });
ValidateSingleClickSelection(SHRT_MAX, { 9, expected_row }, { 9, expected_row });
Log::Comment(L"Double click selection with MAXIMUM scrollback value");
ValidateDoubleClickSelection(SHRT_MAX, { 0, expected_row, 9, expected_row });
ValidateDoubleClickSelection(SHRT_MAX, { 0, expected_row }, { 9, expected_row });
Log::Comment(L"Triple click selection with MAXIMUM scrollback value");
ValidateTripleClickSelection(SHRT_MAX, { 0, expected_row, 9, expected_row });
ValidateTripleClickSelection(SHRT_MAX, { 0, expected_row }, { 9, expected_row });
}
TEST_METHOD(SelectFromOutofBounds)
@ -190,25 +163,25 @@ namespace TerminalCoreUnitTests
// should clamp to right boundary
term.SetSelectionAnchor({ 20, 5 });
Log::Comment(L"Out of bounds: X-value too large");
ValidateSingleRowSelection(term, { rightBoundary, 5, rightBoundary, 5 });
ValidateLinearSelection(term, { rightBoundary, 5 }, { rightBoundary, 5 });
// Case 2: Simulate click past left (x,y) = (-20,5)
// should clamp to left boundary
term.SetSelectionAnchor({ -20, 5 });
Log::Comment(L"Out of bounds: X-value too negative");
ValidateSingleRowSelection(term, { leftBoundary, 5, leftBoundary, 5 });
ValidateLinearSelection(term, { leftBoundary, 5 }, { leftBoundary, 5 });
// Case 3: Simulate click past top (x,y) = (5,-20)
// should clamp to top boundary
term.SetSelectionAnchor({ 5, -20 });
Log::Comment(L"Out of bounds: Y-value too negative");
ValidateSingleRowSelection(term, { 5, topBoundary, 5, topBoundary });
ValidateLinearSelection(term, { 5, topBoundary }, { 5, topBoundary });
// Case 4: Simulate click past bottom (x,y) = (5,20)
// should clamp to bottom boundary
term.SetSelectionAnchor({ 5, 20 });
Log::Comment(L"Out of bounds: Y-value too large");
ValidateSingleRowSelection(term, { 5, bottomBoundary, 5, bottomBoundary });
ValidateLinearSelection(term, { 5, bottomBoundary }, { 5, bottomBoundary });
}
TEST_METHOD(SelectToOutOfBounds)
@ -232,74 +205,22 @@ namespace TerminalCoreUnitTests
// Case 1: Move out of right boundary
Log::Comment(L"Out of bounds: X-value too large");
term.SetSelectionEnd({ 20, 5 });
ValidateSingleRowSelection(term, til::inclusive_rect({ 5, 5, rightBoundary, 5 }));
ValidateLinearSelection(term, { 5, 5 }, { rightBoundary, 5 });
// Case 2: Move out of left boundary
Log::Comment(L"Out of bounds: X-value negative");
term.SetSelectionEnd({ -20, 5 });
ValidateSingleRowSelection(term, { leftBoundary, 5, 5, 5 });
ValidateLinearSelection(term, { leftBoundary, 5 }, { 5, 5 });
// Case 3: Move out of top boundary
Log::Comment(L"Out of bounds: Y-value negative");
term.SetSelectionEnd({ 5, -20 });
{
auto selectionRects = term.GetSelectionRects();
// Validate selection area
VERIFY_ARE_EQUAL(selectionRects.size(), static_cast<size_t>(6));
for (auto selectionRect : selectionRects)
{
auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive();
auto rowValue = selectionRect.BottomInclusive();
if (rowValue == 0)
{
// Verify top line
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 5, rowValue, rightBoundary, rowValue }));
}
else if (rowValue == 5)
{
// Verify last line
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ leftBoundary, rowValue, 5, rowValue }));
}
else
{
// Verify other lines (full)
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ leftBoundary, rowValue, rightBoundary, rowValue }));
}
}
}
ValidateLinearSelection(term, { 5, 0 }, { 5, 5 });
// Case 4: Move out of bottom boundary
Log::Comment(L"Out of bounds: Y-value too large");
term.SetSelectionEnd({ 5, 20 });
{
auto selectionRects = term.GetSelectionRects();
// Validate selection area
VERIFY_ARE_EQUAL(selectionRects.size(), static_cast<size_t>(5));
for (auto selectionRect : selectionRects)
{
auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive();
auto rowValue = selectionRect.BottomInclusive();
if (rowValue == 5)
{
// Verify top line
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 5, 5, rightBoundary, 5 }));
}
else if (rowValue == 9)
{
// Verify bottom line
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ leftBoundary, rowValue, 5, rowValue }));
}
else
{
// Verify other lines (full)
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ leftBoundary, rowValue, rightBoundary, rowValue }));
}
}
}
ValidateLinearSelection(term, { 5, 5 }, { 5, 9 });
}
TEST_METHOD(SelectBoxArea)
@ -321,19 +242,15 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 15, 20 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
auto selectionSpans = term.GetSelectionSpans();
// Validate selection area
VERIFY_ARE_EQUAL(selectionRects.size(), static_cast<size_t>(11));
VERIFY_ARE_EQUAL(selectionSpans.size(), static_cast<size_t>(11));
auto viewport = term.GetViewport();
for (auto selectionRect : selectionRects)
for (auto&& sp : selectionSpans)
{
auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive();
// Verify all lines
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 5, rowValue, 15, rowValue }));
VERIFY_ARE_EQUAL((til::point{ 5, rowValue }), sp.start);
VERIFY_ARE_EQUAL((til::point{ 15, rowValue }), sp.end);
rowValue++;
}
}
@ -342,8 +259,12 @@ namespace TerminalCoreUnitTests
{
Terminal term{ Terminal::TestDummyMarker{} };
DummyRenderer renderer{ &term };
til::CoordType scrollbackLines = 5;
term.Create({ 100, 100 }, scrollbackLines, renderer);
til::CoordType scrollbackLines = 100;
term.Create({ 120, 30 }, scrollbackLines, renderer);
const til::CoordType contentScrollLines = 15;
// Simulate a content-initiated scroll down by 15 lines
term.SetViewportPosition({ 0, contentScrollLines });
// Used for two things:
// - click y-pos
@ -356,36 +277,19 @@ namespace TerminalCoreUnitTests
// Simulate move to (x,y) = (15,20)
term.SetSelectionEnd({ 15, 20 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
ValidateLinearSelection(term, { 5, contentScrollLines + rowValue }, { 15, contentScrollLines + 20 });
// Validate selection area
VERIFY_ARE_EQUAL(selectionRects.size(), static_cast<size_t>(11));
const til::CoordType userScrollViewportTop = 10;
// Simulate a user-initiated scroll *up* to line 10 (NOTE: Not *up by 10 lines*)
term.UserScrollViewport(userScrollViewportTop);
auto viewport = term.GetViewport();
auto rightBoundary = viewport.RightInclusive();
for (auto selectionRect : selectionRects)
{
auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive();
// Simulate click at (x,y) = (5,10)
term.SetSelectionAnchor({ 5, rowValue });
if (rowValue == 10)
{
// Verify top line
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 5, 10, rightBoundary, 10 }));
}
else if (rowValue == 20)
{
// Verify bottom line
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 0, 20, 15, 20 }));
}
else
{
// Verify other lines (full)
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 0, rowValue, rightBoundary, rowValue }));
}
// Simulate move to (x,y) = (15,20)
term.SetSelectionEnd({ 15, 20 });
rowValue++;
}
ValidateLinearSelection(term, { 5, userScrollViewportTop + rowValue }, { 15, userScrollViewportTop + 20 });
}
TEST_METHOD(SelectWideGlyph_Trailing)
@ -408,7 +312,7 @@ namespace TerminalCoreUnitTests
// Validate selection area
// Selection should expand one to the left to get the leading half of the wide glyph
ValidateSingleRowSelection(term, { 4, 10, 5, 10 });
ValidateLinearSelection(term, { 4, 10 }, { 5, 10 });
}
TEST_METHOD(SelectWideGlyph_Leading)
@ -431,7 +335,7 @@ namespace TerminalCoreUnitTests
// Validate selection area
// Selection should expand one to the left to get the leading half of the wide glyph
ValidateSingleRowSelection(term, { 4, 10, 5, 10 });
ValidateLinearSelection(term, { 4, 10 }, { 5, 10 });
}
TEST_METHOD(SelectWideGlyphsInBoxSelection)
@ -460,29 +364,30 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 7, 12 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
auto selectionSpans = term.GetSelectionSpans();
// Validate selection area
VERIFY_ARE_EQUAL(selectionRects.size(), static_cast<size_t>(5));
VERIFY_ARE_EQUAL(selectionSpans.size(), static_cast<size_t>(5));
auto viewport = term.GetViewport();
til::CoordType rowValue = 8;
for (auto selectionRect : selectionRects)
for (auto&& sp : selectionSpans)
{
auto selection = viewport.ConvertToOrigin(selectionRect).ToInclusive();
if (rowValue == 10)
{
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 4, rowValue, 7, rowValue }));
VERIFY_ARE_EQUAL((til::point{ 4, rowValue }), sp.start);
VERIFY_ARE_EQUAL((til::point{ 7, rowValue }), sp.end);
}
else if (rowValue == 11)
{
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 5, rowValue, 8, rowValue }));
VERIFY_ARE_EQUAL((til::point{ 5, rowValue }), sp.start);
VERIFY_ARE_EQUAL((til::point{ 8, rowValue }), sp.end);
}
else
{
// Verify all lines
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 5, rowValue, 7, rowValue }));
VERIFY_ARE_EQUAL((til::point{ 5, rowValue }), sp.start);
VERIFY_ARE_EQUAL((til::point{ 7, rowValue }), sp.end);
}
rowValue++;
@ -509,7 +414,7 @@ namespace TerminalCoreUnitTests
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Word);
// Validate selection area
ValidateSingleRowSelection(term, til::inclusive_rect{ 4, 10, gsl::narrow<til::CoordType>(4 + text.size() - 1), 10 });
ValidateLinearSelection(term, { 4, 10 }, { gsl::narrow<til::CoordType>(4 + text.size() - 1), 10 });
}
TEST_METHOD(DoubleClick_Delimiter)
@ -526,11 +431,8 @@ namespace TerminalCoreUnitTests
auto clickPos = til::point{ 5, 10 };
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Word);
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
// Validate selection area
ValidateSingleRowSelection(term, til::inclusive_rect({ 0, 10, 99, 10 }));
ValidateLinearSelection(term, { 0, 10 }, { 99, 10 });
}
TEST_METHOD(DoubleClick_DelimiterClass)
@ -558,7 +460,7 @@ namespace TerminalCoreUnitTests
// ">" is in class 1
// the white space to the right of the ">" is in class 0
// Double-clicking the ">" should only highlight that cell
ValidateSingleRowSelection(term, til::inclusive_rect({ 15, 10, 15, 10 }));
ValidateLinearSelection(term, { 15, 10 }, { 15, 10 });
}
TEST_METHOD(DoubleClickDrag_Right)
@ -587,7 +489,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 21, 10 });
// Validate selection area
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 32, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 32, 10 });
}
TEST_METHOD(DoubleClickDrag_Left)
@ -616,7 +518,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 5, 10 });
// Validate selection area
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 32, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 32, 10 });
}
TEST_METHOD(TripleClick_GeneralCase)
@ -630,7 +532,7 @@ namespace TerminalCoreUnitTests
term.MultiClickSelection(clickPos, Terminal::SelectionExpansion::Line);
// Validate selection area
ValidateSingleRowSelection(term, til::inclusive_rect({ 0, 10, 99, 10 }));
ValidateLinearSelection(term, { 0, 10 }, { 99, 10 });
}
TEST_METHOD(TripleClickDrag_Horizontal)
@ -647,7 +549,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 7, 10 });
// Validate selection area
ValidateSingleRowSelection(term, til::inclusive_rect({ 0, 10, 99, 10 }));
ValidateLinearSelection(term, { 0, 10 }, { 99, 10 });
}
TEST_METHOD(TripleClickDrag_Vertical)
@ -663,19 +565,7 @@ namespace TerminalCoreUnitTests
// Simulate move to (x,y) = (5,11)
term.SetSelectionEnd({ 5, 11 });
// Simulate renderer calling TriggerSelection and acquiring selection area
auto selectionRects = term.GetSelectionRects();
// Validate selection area
VERIFY_ARE_EQUAL(selectionRects.size(), static_cast<size_t>(2));
// verify first selection rect
auto selection = term.GetViewport().ConvertToOrigin(selectionRects.at(0)).ToInclusive();
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 0, 10, 99, 10 }));
// verify second selection rect
selection = term.GetViewport().ConvertToOrigin(selectionRects.at(1)).ToInclusive();
VERIFY_ARE_EQUAL(selection, til::inclusive_rect({ 0, 11, 99, 11 }));
ValidateLinearSelection(term, { 0, 10 }, { 99, 11 });
}
TEST_METHOD(ShiftClick)
@ -699,7 +589,7 @@ namespace TerminalCoreUnitTests
term.MultiClickSelection({ 5, 10 }, Terminal::SelectionExpansion::Word);
// Validate selection area: "doubleClickMe" selected
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 16, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 16, 10 });
}
// Step 2: Shift+Click to "dragThroughHere"
@ -712,7 +602,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Char);
// Validate selection area: "doubleClickMe drag" selected
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 21, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 21, 10 });
}
// Step 3: Shift+Double-Click at "dragThroughHere"
@ -725,7 +615,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Word);
// Validate selection area: "doubleClickMe dragThroughHere" selected
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 32, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 32, 10 });
}
// Step 4: Shift+Triple-Click at "dragThroughHere"
@ -738,7 +628,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Line);
// Validate selection area: "doubleClickMe dragThroughHere..." selected
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 99, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 99, 10 });
}
// Step 5: Shift+Double-Click at "dragThroughHere"
@ -751,7 +641,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 21, 10 }, Terminal::SelectionExpansion::Word);
// Validate selection area: "doubleClickMe dragThroughHere" selected
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 32, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 32, 10 });
}
// Step 6: Drag past "dragThroughHere"
@ -765,7 +655,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 35, 10 });
// Validate selection area: "doubleClickMe dragThroughHere..." selected
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 99, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 99, 10 });
}
// Step 6: Drag back to "dragThroughHere"
@ -778,7 +668,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 21, 10 });
// Validate selection area: "doubleClickMe dragThroughHere" selected
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 32, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 32, 10 });
}
// Step 7: Drag within "dragThroughHere"
@ -791,7 +681,7 @@ namespace TerminalCoreUnitTests
term.SetSelectionEnd({ 25, 10 });
// Validate selection area: "doubleClickMe dragThroughHere" still selected
ValidateSingleRowSelection(term, til::inclusive_rect({ 4, 10, 32, 10 }));
ValidateLinearSelection(term, { 4, 10 }, { 32, 10 });
}
}
@ -807,7 +697,7 @@ namespace TerminalCoreUnitTests
term.SelectNewRegion({ 10, 10 }, { 20, 10 });
// Validate selection area
ValidateSingleRowSelection(term, til::inclusive_rect({ 10, 10, 20, 10 }));
ValidateLinearSelection(term, { 10, 10 }, { 20, 10 });
}
// Step 2: Drag to (5,10)
@ -816,7 +706,7 @@ namespace TerminalCoreUnitTests
// Validate selection area
// NOTE: Pivot should be (10, 10)
ValidateSingleRowSelection(term, til::inclusive_rect({ 5, 10, 10, 10 }));
ValidateLinearSelection(term, { 5, 10 }, { 10, 10 });
}
// Step 3: Drag back to (20,10)
@ -825,7 +715,7 @@ namespace TerminalCoreUnitTests
// Validate selection area
// NOTE: Pivot should still be (10, 10)
ValidateSingleRowSelection(term, til::inclusive_rect({ 10, 10, 20, 10 }));
ValidateLinearSelection(term, { 10, 10 }, { 20, 10 });
}
// Step 4: Shift+Click at (5,10)
@ -834,7 +724,7 @@ namespace TerminalCoreUnitTests
// Validate selection area
// NOTE: Pivot should still be (10, 10)
ValidateSingleRowSelection(term, til::inclusive_rect({ 5, 10, 10, 10 }));
ValidateLinearSelection(term, { 5, 10 }, { 10, 10 });
}
// Step 5: Shift+Click back at (20,10)
@ -843,7 +733,7 @@ namespace TerminalCoreUnitTests
// Validate selection area
// NOTE: Pivot should still be (10, 10)
ValidateSingleRowSelection(term, til::inclusive_rect({ 10, 10, 20, 10 }));
ValidateLinearSelection(term, { 10, 10 }, { 20, 10 });
}
}
};

View File

@ -59,24 +59,11 @@ const FontInfo& RenderData::GetFontInfo() const noexcept
}
// Method Description:
// - Retrieves one rectangle per line describing the area of the viewport
// - Retrieves one span per line describing the area of the viewport
// that should be highlighted in some way to represent a user-interactive selection
// Return Value:
// - Vector of Viewports describing the area selected
std::vector<Viewport> RenderData::GetSelectionRects() noexcept
std::span<const til::point_span> RenderData::GetSelectionSpans() const noexcept
{
std::vector<Viewport> result;
try
{
for (const auto& select : Selection::Instance().GetSelectionRects())
{
result.emplace_back(Viewport::FromInclusive(select));
}
}
CATCH_LOG();
return result;
return Selection::Instance().GetSelectionSpans();
}
// Method Description:
@ -368,7 +355,7 @@ const til::point RenderData::GetSelectionEnd() const noexcept
// - SelectionAnchor: the initial position where the selection was started
// - SelectionRect: the rectangular region denoting a portion of the buffer that is selected
// The following is an excerpt from Selection::s_GetSelectionRects
// The following is an excerpt from Selection::GetSelectionSpans
// 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.

View File

@ -25,7 +25,7 @@ public:
TextBuffer& GetTextBuffer() const noexcept override;
const FontInfo& GetFontInfo() const noexcept override;
std::vector<Microsoft::Console::Types::Viewport> GetSelectionRects() noexcept override;
std::span<const til::point_span> GetSelectionSpans() const noexcept override;
void LockConsole() noexcept override;
void UnlockConsole() noexcept override;

View File

@ -20,14 +20,14 @@ Selection& Selection::Instance()
return *_instance;
}
void Selection::_RegenerateSelectionRects() const
void Selection::_RegenerateSelectionSpans() const
{
if (_lastSelectionGeneration == _d.generation())
{
return;
}
_lastSelectionRects.clear();
_lastSelectionSpans.clear();
if (!_d->fSelectionVisible)
{
@ -44,23 +44,17 @@ void Selection::_RegenerateSelectionRects() const
endSelectionAnchor.y = (_d->coordSelectionAnchor.y == _d->srSelectionRect.top) ? _d->srSelectionRect.bottom : _d->srSelectionRect.top;
const auto blockSelection = !IsLineSelection();
auto rects = screenInfo.GetTextBuffer().GetTextRects(_d->coordSelectionAnchor, endSelectionAnchor, blockSelection, false);
_lastSelectionRects = std::move(rects);
_lastSelectionSpans = screenInfo.GetTextBuffer().GetTextSpans(_d->coordSelectionAnchor,
endSelectionAnchor,
blockSelection,
false);
_lastSelectionGeneration = _d.generation();
}
// Routine Description:
// - Determines the line-by-line selection rectangles based on global selection state.
// Arguments:
// - <none> - Uses internal state to know what area is selected already.
// Return Value:
// - Returns a vector where each til::inclusive_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<til::inclusive_rect> Selection::GetSelectionRects() const
std::span<const til::point_span> Selection::GetSelectionSpans() const
{
_RegenerateSelectionRects();
return _lastSelectionRects;
_RegenerateSelectionSpans();
return { _lastSelectionSpans.cbegin(), _lastSelectionSpans.cend() };
}
// Routine Description:

View File

@ -29,7 +29,7 @@ class Selection
public:
~Selection() = default;
std::vector<til::inclusive_rect> GetSelectionRects() const;
std::span<const til::point_span> GetSelectionSpans() const;
void ShowSelection();
void HideSelection();
@ -182,11 +182,11 @@ private:
};
til::generational<SelectionData> _d{};
mutable std::vector<til::inclusive_rect> _lastSelectionRects;
mutable std::vector<til::point_span> _lastSelectionSpans;
mutable til::generation_t _lastSelectionGeneration;
void _ExtendSelection(SelectionData* d, _In_ til::point coordBufferPos);
void _RegenerateSelectionRects() const;
void _RegenerateSelectionSpans() const;
#ifdef UNIT_TESTING
friend class SelectionTests;

View File

@ -671,51 +671,35 @@ bool Selection::_HandleColorSelection(const INPUT_KEY_INFO* const pInputKeyInfo)
selectionAttr.SetIndexedForeground256(colorIndex);
}
// If shift was pressed as well, then this is actually a
// find-and-color request. Otherwise just color the selection.
const auto& textBuffer = gci.renderData.GetTextBuffer();
if (fShiftPressed)
{
try
// Search the selection and color *that*
const auto req = TextBuffer::CopyRequest::FromConfig(textBuffer,
til::point{ _d->srSelectionRect.left, _d->srSelectionRect.top },
til::point{ _d->srSelectionRect.right, _d->srSelectionRect.bottom },
true /* multi-line search doesn't make sense; concatenate all lines */,
false /* we filtered out block search above */,
true /* trim block selection */,
true);
const auto str = textBuffer.GetPlainText(req);
// Clear the selection and call the search / mark function.
ClearSelection();
const auto hits = textBuffer.SearchText(str, SearchFlag::CaseInsensitive).value_or(std::vector<til::point_span>{});
for (const auto& s : hits)
{
const auto selectionRects = GetSelectionRects();
if (selectionRects.size() > 0)
{
// Pull the selection out of the buffer to pass to the
// search function. Clamp to max search string length.
// We just copy the bytes out of the row buffer.
std::wstring str;
for (const auto& selectRect : selectionRects)
{
auto it = screenInfo.GetCellDataAt({ selectRect.left, selectRect.top });
for (til::CoordType i = 0; i < (selectRect.right - selectRect.left + 1);)
{
str.append(it->Chars());
i += it->Columns();
it += it->Columns();
}
}
// Clear the selection and call the search / mark function.
ClearSelection();
const auto& textBuffer = gci.renderData.GetTextBuffer();
const auto hits = textBuffer.SearchText(str, SearchFlag::CaseInsensitive).value_or(std::vector<til::point_span>{});
for (const auto& s : hits)
{
ColorSelection(s.start, s.end, selectionAttr);
}
}
ColorSelection(s.start, s.end, selectionAttr);
}
CATCH_LOG();
}
else
{
const auto selectionRects = GetSelectionRects();
for (const auto& selectionRect : selectionRects)
const auto selection = GetSelectionSpans();
for (auto&& sp : selection)
{
ColorSelection(selectionRect, selectionAttr);
sp.iterate_rows(textBuffer.GetSize().Width(), [&](til::CoordType row, til::CoordType beg, til::CoordType end) {
ColorSelection({ beg, row, end, row }, selectionAttr);
});
}
ClearSelection();
}

View File

@ -56,30 +56,30 @@ class SelectionTests
return true;
}
void VerifyGetSelectionRects_BoxMode()
void VerifyGetSelectionSpans_BoxMode()
{
const auto selectionRects = m_pSelection->GetSelectionRects();
const auto selectionSpans = m_pSelection->GetSelectionSpans();
const UINT cRectanglesExpected = m_pSelection->_d->srSelectionRect.bottom - m_pSelection->_d->srSelectionRect.top + 1;
if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionRects.size()))
if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionSpans.size()))
{
for (auto iRect = 0; iRect < gsl::narrow<int>(selectionRects.size()); iRect++)
for (auto iRect = 0; iRect < gsl::narrow<int>(selectionSpans.size()); iRect++)
{
// ensure each rectangle is exactly the width requested (block selection)
const auto psrRect = &selectionRects[iRect];
const auto& span = selectionSpans[iRect];
const auto sRectangleLineNumber = (til::CoordType)iRect + m_pSelection->_d->srSelectionRect.top;
VERIFY_ARE_EQUAL(psrRect->top, sRectangleLineNumber);
VERIFY_ARE_EQUAL(psrRect->bottom, sRectangleLineNumber);
VERIFY_ARE_EQUAL(span.start.y, sRectangleLineNumber);
VERIFY_ARE_EQUAL(span.end.y, sRectangleLineNumber);
VERIFY_ARE_EQUAL(psrRect->left, m_pSelection->_d->srSelectionRect.left);
VERIFY_ARE_EQUAL(psrRect->right, m_pSelection->_d->srSelectionRect.right);
VERIFY_ARE_EQUAL(span.start.x, m_pSelection->_d->srSelectionRect.left);
VERIFY_ARE_EQUAL(span.end.x, m_pSelection->_d->srSelectionRect.right);
}
}
}
TEST_METHOD(TestGetSelectionRects_BoxMode)
TEST_METHOD(TestGetSelectionSpans_BoxMode)
{
{
auto selection{ m_pSelection->_d.write() };
@ -99,7 +99,7 @@ class SelectionTests
selection->fLineSelection = false;
selection->fUseAlternateSelection = false;
VerifyGetSelectionRects_BoxMode();
VerifyGetSelectionSpans_BoxMode();
}
{
@ -108,7 +108,7 @@ class SelectionTests
selection->fLineSelection = true;
selection->fUseAlternateSelection = true;
VerifyGetSelectionRects_BoxMode();
VerifyGetSelectionSpans_BoxMode();
}
{
@ -118,7 +118,7 @@ class SelectionTests
selection->coordSelectionAnchor.x = selection->srSelectionRect.right;
selection->coordSelectionAnchor.y = selection->srSelectionRect.top;
VerifyGetSelectionRects_BoxMode();
VerifyGetSelectionSpans_BoxMode();
}
{
@ -127,129 +127,35 @@ class SelectionTests
selection->coordSelectionAnchor.x = selection->srSelectionRect.left;
selection->coordSelectionAnchor.y = selection->srSelectionRect.bottom;
VerifyGetSelectionRects_BoxMode();
VerifyGetSelectionSpans_BoxMode();
}
{
auto selection{ m_pSelection->_d.write() };
// #4 bottom-right to top-left selection
selection->coordSelectionAnchor.x = selection->srSelectionRect.right;
selection->coordSelectionAnchor.y = selection->srSelectionRect.bottom;
VerifyGetSelectionRects_BoxMode();
VerifyGetSelectionSpans_BoxMode();
}
}
void VerifyGetSelectionRects_LineMode()
void VerifyGetSelectionSpans_LineMode(const til::point start, const til::point end)
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto selectionSpans = m_pSelection->GetSelectionSpans();
const auto selectionRects = m_pSelection->GetSelectionRects();
const UINT cRectanglesExpected = m_pSelection->_d->srSelectionRect.bottom - m_pSelection->_d->srSelectionRect.top + 1;
if (VERIFY_ARE_EQUAL(cRectanglesExpected, selectionRects.size()))
if (VERIFY_ARE_EQUAL(1u, selectionSpans.size()))
{
// RULES:
// 1. If we're only selection one line, select the entire region between the two rectangles.
// Else if we're selecting multiple lines...
// 2. Extend all lines except the last line to the right edge of the screen
// Extend all lines except the first line to the left edge of the screen
// 3. If our anchor is in the top-right or bottom-left corner of the rectangle...
// The inside portion of our rectangle on the first and last lines is invalid.
// Remove from selection (but preserve the anchors themselves).
// RULE #1: If 1 line, entire region selected.
auto fHaveOneLine = selectionRects.size() == 1;
if (fHaveOneLine)
{
auto srSelectionRect = m_pSelection->_d->srSelectionRect;
VERIFY_ARE_EQUAL(srSelectionRect.top, srSelectionRect.bottom);
const auto psrRect = &selectionRects[0];
VERIFY_ARE_EQUAL(psrRect->top, srSelectionRect.top);
VERIFY_ARE_EQUAL(psrRect->bottom, srSelectionRect.bottom);
VERIFY_ARE_EQUAL(psrRect->left, srSelectionRect.left);
VERIFY_ARE_EQUAL(psrRect->right, srSelectionRect.right);
}
else
{
// RULE #2 : Check extension to edges
for (UINT iRect = 0; iRect < selectionRects.size(); iRect++)
{
// ensure each rectangle is exactly the width requested (block selection)
const auto psrRect = &selectionRects[iRect];
const auto sRectangleLineNumber = (til::CoordType)iRect + m_pSelection->_d->srSelectionRect.top;
VERIFY_ARE_EQUAL(psrRect->top, sRectangleLineNumber);
VERIFY_ARE_EQUAL(psrRect->bottom, sRectangleLineNumber);
auto fIsFirstLine = iRect == 0;
auto fIsLastLine = iRect == selectionRects.size() - 1;
// for all lines except the last, the line should reach the right edge of the buffer
if (!fIsLastLine)
{
// buffer size = 80, then selection goes 0 to 79. Thus X - 1.
VERIFY_ARE_EQUAL(psrRect->right, gci.GetActiveOutputBuffer().GetTextBuffer().GetSize().RightInclusive());
}
// for all lines except the first, the line should reach the left edge of the buffer
if (!fIsFirstLine)
{
VERIFY_ARE_EQUAL(psrRect->left, 0);
}
}
// RULE #3: Check first and last line have invalid regions removed, if applicable
UINT iFirst = 0;
auto iLast = gsl::narrow<UINT>(selectionRects.size() - 1u);
const auto psrFirst = &selectionRects[iFirst];
const auto psrLast = &selectionRects[iLast];
auto fRemoveRegion = false;
auto srSelectionRect = m_pSelection->_d->srSelectionRect;
auto coordAnchor = m_pSelection->_d->coordSelectionAnchor;
// if the anchor is in the top right or bottom left corner, we must have removed a region. otherwise, it stays as is.
if (coordAnchor.y == srSelectionRect.top && coordAnchor.x == srSelectionRect.right)
{
fRemoveRegion = true;
}
else if (coordAnchor.y == srSelectionRect.bottom && coordAnchor.x == srSelectionRect.left)
{
fRemoveRegion = true;
}
// now check the first row's left based on removal
if (!fRemoveRegion)
{
VERIFY_ARE_EQUAL(psrFirst->left, srSelectionRect.left);
}
else
{
VERIFY_ARE_EQUAL(psrFirst->left, srSelectionRect.right);
}
// and the last row's right based on removal
if (!fRemoveRegion)
{
VERIFY_ARE_EQUAL(psrLast->right, srSelectionRect.right);
}
else
{
VERIFY_ARE_EQUAL(psrLast->right, srSelectionRect.left);
}
}
auto& span{ selectionSpans[0] };
VERIFY_ARE_EQUAL(start, span.start, L"start");
VERIFY_ARE_EQUAL(end, span.end, L"end");
}
}
TEST_METHOD(TestGetSelectionRects_LineMode)
// All of the logic tested herein is trying to determine where the selection
// must have started, given a rectangle and the point where the mouse was last seen.
TEST_METHOD(TestGetSelectionSpans_LineMode)
{
{
auto selection{ m_pSelection->_d.write() };
@ -262,6 +168,16 @@ class SelectionTests
selection->srSelectionRect.left = 1;
selection->srSelectionRect.right = 10;
/*
| RECT |
0123456789ABCDEF
--0+---------+
1| |
2| |
--3+---------+
4
*/
// #1 top-left to bottom right selection first
selection->coordSelectionAnchor.x = selection->srSelectionRect.left;
selection->coordSelectionAnchor.y = selection->srSelectionRect.top;
@ -270,7 +186,19 @@ class SelectionTests
selection->fLineSelection = true;
selection->fUseAlternateSelection = false;
VerifyGetSelectionRects_LineMode();
/*
Mouse at 0,0; therefore, the selection "begins" at 3,10
Selection extends to bottom right corner of rectangle
| RECT |
0123456789ABCDEF
--0*#########*#####
1################
2################
--3*#########*
4
*/
VerifyGetSelectionSpans_LineMode({ 1, 0 }, { 10, 3 });
}
{
@ -279,7 +207,8 @@ class SelectionTests
selection->fLineSelection = false;
selection->fUseAlternateSelection = true;
VerifyGetSelectionRects_LineMode();
// Same as above.
VerifyGetSelectionSpans_LineMode({ 1, 0 }, { 10, 3 });
}
{
@ -289,29 +218,69 @@ class SelectionTests
selection->coordSelectionAnchor.x = selection->srSelectionRect.right;
selection->coordSelectionAnchor.y = selection->srSelectionRect.top;
VerifyGetSelectionRects_LineMode();
/*
Mouse at 0,10; therefore, the selection must have started at 3,0
Selection does not include bottom-most line
| RECT |
0123456789ABCDEF
--0+ *#####
1################
2################
--3* +
4
*/
VerifyGetSelectionSpans_LineMode({ 10, 0 }, { 1, 3 });
}
{
auto selection{ m_pSelection->_d.write() };
// #3 bottom-left to top-right selection
selection->coordSelectionAnchor.x = selection->srSelectionRect.left;
selection->coordSelectionAnchor.y = selection->srSelectionRect.bottom;
VerifyGetSelectionRects_LineMode();
/*
Mouse at 3,1; therefore, the selection must have started at 0,10
Selection extends from top right to bottom left
| RECT |
0123456789ABCDEF
--0+ *#####
1################
2################
--3* +
4
*/
VerifyGetSelectionSpans_LineMode({ 10, 0 }, { 1, 3 });
}
{
auto selection{ m_pSelection->_d.write() };
// #4 bottom-right to top-left selection
selection->coordSelectionAnchor.x = selection->srSelectionRect.right;
selection->coordSelectionAnchor.y = selection->srSelectionRect.bottom;
VerifyGetSelectionRects_LineMode();
/*
Mouse at 3,10; therefore, the selection must have started at 0,0
Just like case #1, selection covers all lines and top left/bottom right of rect.
| RECT |
0123456789ABCDEF
--0*#########*#####
1################
2################
--3*#########*
4
*/
VerifyGetSelectionSpans_LineMode({ 1, 0 }, { 10, 3 });
}
{
auto selection{ m_pSelection->_d.write() };
// Part II: Single line selection
selection->srSelectionRect.top = 2;
selection->srSelectionRect.bottom = 2;
@ -323,17 +292,18 @@ class SelectionTests
VERIFY_IS_TRUE(selection->srSelectionRect.bottom == selection->srSelectionRect.top);
selection->coordSelectionAnchor.y = selection->srSelectionRect.bottom;
VerifyGetSelectionRects_LineMode();
VerifyGetSelectionSpans_LineMode({ 1, 2 }, { 10, 2 });
}
{
auto selection{ m_pSelection->_d.write() };
// #2: right to left selection
selection->coordSelectionAnchor.x = selection->srSelectionRect.right;
VERIFY_IS_TRUE(selection->srSelectionRect.bottom == selection->srSelectionRect.top);
selection->coordSelectionAnchor.y = selection->srSelectionRect.top;
VerifyGetSelectionRects_LineMode();
VerifyGetSelectionSpans_LineMode({ 1, 2 }, { 10, 2 });
}
}

View File

@ -48,11 +48,6 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid
return S_OK;
}
[[nodiscard]] HRESULT BgfxEngine::InvalidateSelection(const std::vector<til::rect>& /*rectangles*/) noexcept
{
return S_OK;
}
[[nodiscard]] HRESULT BgfxEngine::InvalidateScroll(const til::point* /*pcoordDelta*/) noexcept
{
return S_OK;

View File

@ -35,7 +35,6 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT Invalidate(const til::rect* psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;

View File

@ -321,9 +321,6 @@ void Clipboard::StoreSelectionToClipboard(const bool copyFormatting)
return;
}
// read selection area.
const auto selectionRects = selection.GetSelectionRects();
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto& buffer = gci.GetActiveOutputBuffer().GetTextBuffer();
const auto& renderSettings = gci.GetRenderSettings();

View File

@ -83,27 +83,13 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2<T>& out) noexcept
return Invalidate(&rect);
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept
{
for (const auto& rect : rectangles)
{
// BeginPaint() protects against invalid out of bounds numbers.
// TODO: rect can contain invalid out of bounds coordinates when the selection is being
// dragged outside of the viewport (and the window begins scrolling automatically).
_api.invalidatedRows.start = gsl::narrow_cast<u16>(std::min<int>(_api.invalidatedRows.start, std::max<int>(0, rect.top)));
_api.invalidatedRows.end = gsl::narrow_cast<u16>(std::max<int>(_api.invalidatedRows.end, std::max<int>(0, rect.bottom)));
}
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept
void AtlasEngine::_invalidateSpans(std::span<const til::point_span> spans, const TextBuffer& buffer) noexcept
{
const auto viewportOrigin = til::point{ _api.viewportOffset.x, _api.viewportOffset.y };
const auto viewport = til::rect{ 0, 0, _api.s->viewportCellCount.x, _api.s->viewportCellCount.y };
const auto cellCountX = static_cast<til::CoordType>(_api.s->viewportCellCount.x);
for (const auto& hi : highlights)
for (auto&& sp : spans)
{
hi.iterate_rows(cellCountX, [&](til::CoordType row, til::CoordType beg, til::CoordType end) {
sp.iterate_rows(til::CoordTypeMax, [&](til::CoordType row, til::CoordType beg, til::CoordType end) {
const auto shift = buffer.GetLineRendition(row) != LineRendition::SingleWidth ? 1 : 0;
beg <<= shift;
end <<= shift;
@ -114,7 +100,22 @@ constexpr HRESULT vec2_narrow(U x, U y, vec2<T>& out) noexcept
_api.invalidatedRows.end = gsl::narrow_cast<u16>(std::max<int>(_api.invalidatedRows.end, std::max<int>(0, rect.bottom)));
});
}
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateSelection(std::span<const til::rect> selections) noexcept
{
if (!selections.empty())
{
// INVARIANT: This assumes that `selections` is sorted by increasing Y
_api.invalidatedRows.start = gsl::narrow_cast<u16>(std::min<int>(_api.invalidatedRows.start, std::max<int>(0, selections.front().top)));
_api.invalidatedRows.end = gsl::narrow_cast<u16>(std::max<int>(_api.invalidatedRows.end, std::max<int>(0, selections.back().bottom)));
}
return S_OK;
}
[[nodiscard]] HRESULT AtlasEngine::InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept
{
_invalidateSpans(highlights, buffer);
return S_OK;
}

View File

@ -31,7 +31,7 @@ namespace Microsoft::Console::Render::Atlas
[[nodiscard]] HRESULT Invalidate(const til::rect* psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(std::span<const til::rect> selections) noexcept override;
[[nodiscard]] HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;
@ -98,6 +98,7 @@ namespace Microsoft::Console::Render::Atlas
[[nodiscard]] HRESULT _updateFont(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, const std::unordered_map<std::wstring_view, float>& features, const std::unordered_map<std::wstring_view, float>& axes) noexcept;
void _resolveFontMetrics(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontSettings* fontMetrics = nullptr);
[[nodiscard]] bool _updateWithNearbyFontCollection() noexcept;
void _invalidateSpans(std::span<const til::point_span> spans, const TextBuffer& buffer) noexcept;
// AtlasEngine.r.cpp
ATLAS_ATTR_COLD void _recreateAdapter();

View File

@ -7,6 +7,11 @@
using namespace Microsoft::Console;
using namespace Microsoft::Console::Render;
[[nodiscard]] HRESULT RenderEngineBase::InvalidateSelection(std::span<const til::rect> /*selections*/) noexcept
{
return S_OK;
}
[[nodiscard]] HRESULT RenderEngineBase::InvalidateHighlight(std::span<const til::point_span> /*highlights*/, const TextBuffer& /*renditions*/) noexcept
{
return S_OK;

View File

@ -325,32 +325,46 @@ void Renderer::TriggerTeardown() noexcept
// Return Value:
// - <none>
void Renderer::TriggerSelection()
try
{
try
const auto spans = _pData->GetSelectionSpans();
if (spans.size() != _lastSelectionPaintSize || (!spans.empty() && _lastSelectionPaintSpan != til::point_span{ spans.front().start, spans.back().end }))
{
// Get selection rectangles
auto rects = _GetSelectionRects();
std::vector<til::rect> newSelectionViewportRects;
// Make a viewport representing the coordinates that are currently presentable.
const til::rect viewport{ _pData->GetViewport().Dimensions() };
// Restrict all previous selection rectangles to inside the current viewport bounds
for (auto& sr : _previousSelection)
_lastSelectionPaintSize = spans.size();
if (_lastSelectionPaintSize)
{
sr &= viewport;
_lastSelectionPaintSpan = til::point_span{ spans.front().start, spans.back().end };
const auto& buffer = _pData->GetTextBuffer();
const auto bufferWidth = buffer.GetSize().Width();
const til::rect vp{ _viewport.ToExclusive() };
for (auto&& sp : spans)
{
sp.iterate_rows(bufferWidth, [&](til::CoordType row, til::CoordType min, til::CoordType max) {
const auto shift = buffer.GetLineRendition(row) != LineRendition::SingleWidth ? 1 : 0;
max += 1; // Selection spans are inclusive (still)
min <<= shift;
max <<= shift;
til::rect r{ min, row, max, row + 1 };
newSelectionViewportRects.emplace_back(r.to_origin(vp));
});
}
}
FOREACH_ENGINE(pEngine)
{
LOG_IF_FAILED(pEngine->InvalidateSelection(_previousSelection));
LOG_IF_FAILED(pEngine->InvalidateSelection(rects));
LOG_IF_FAILED(pEngine->InvalidateSelection(_lastSelectionRectsByViewport));
LOG_IF_FAILED(pEngine->InvalidateSelection(newSelectionViewportRects));
}
_previousSelection = std::move(rects);
std::exchange(_lastSelectionRectsByViewport, newSelectionViewportRects);
NotifyPaintFrame();
}
CATCH_LOG();
}
CATCH_LOG()
// Routine Description:
// - Called when the search highlight areas in the console have changed.
@ -1261,6 +1275,7 @@ void Renderer::_PaintCursor(_In_ IRenderEngine* const pEngine)
RenderFrameInfo info;
info.searchHighlights = _pData->GetSearchHighlights();
info.searchHighlightFocused = _pData->GetSearchHighlightFocused();
info.selectionSpans = _pData->GetSelectionSpans();
return pEngine->PrepareRenderInfo(std::move(info));
}
@ -1277,15 +1292,11 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine)
std::span<const til::rect> dirtyAreas;
LOG_IF_FAILED(pEngine->GetDirtyArea(dirtyAreas));
// Get selection rectangles
const auto rectangles = _GetSelectionRects();
std::vector<til::rect> dirtySearchRectangles;
for (auto& dirtyRect : dirtyAreas)
for (auto&& dirtyRect : dirtyAreas)
{
for (const auto& rect : rectangles)
for (const auto& rect : _lastSelectionRectsByViewport)
{
if (const auto rectCopy = rect & dirtyRect)
if (const auto rectCopy{ rect & dirtyRect })
{
LOG_IF_FAILED(pEngine->PaintSelection(rectCopy));
}
@ -1330,32 +1341,6 @@ void Renderer::_PaintSelection(_In_ IRenderEngine* const pEngine)
return pEngine->ScrollFrame();
}
// Routine Description:
// - Helper to determine the selected region of the buffer.
// Return Value:
// - A vector of rectangles representing the regions to select, line by line.
std::vector<til::rect> Renderer::_GetSelectionRects() const
{
const auto& buffer = _pData->GetTextBuffer();
auto rects = _pData->GetSelectionRects();
// Adjust rectangles to viewport
auto view = _pData->GetViewport();
std::vector<til::rect> result;
result.reserve(rects.size());
for (auto rect : rects)
{
// Convert buffer offsets to the equivalent range of screen cells
// expected by callers, taking line rendition into account.
const auto lineRendition = buffer.GetLineRendition(rect.Top());
rect = Viewport::FromInclusive(BufferToScreenLine(rect.ToInclusive(), lineRendition));
result.emplace_back(view.ConvertToOrigin(rect).ToExclusive());
}
return result;
}
// Method Description:
// - Offsets all of the selection rectangles we might be holding onto
// as the previously selected area. If the whole viewport scrolls,
@ -1369,7 +1354,7 @@ void Renderer::_ScrollPreviousSelection(const til::point delta)
{
if (delta != til::point{ 0, 0 })
{
for (auto& rc : _previousSelection)
for (auto& rc : _lastSelectionRectsByViewport)
{
rc += delta;
}

View File

@ -109,7 +109,6 @@ namespace Microsoft::Console::Render
void _PaintCursor(_In_ IRenderEngine* const pEngine);
[[nodiscard]] HRESULT _UpdateDrawingBrushes(_In_ IRenderEngine* const pEngine, const TextAttribute attr, const bool usingSoftFont, const bool isSettingDefaultBrushes);
[[nodiscard]] HRESULT _PerformScrolling(_In_ IRenderEngine* const pEngine);
std::vector<til::rect> _GetSelectionRects() const;
void _ScrollPreviousSelection(const til::point delta);
[[nodiscard]] HRESULT _PaintTitle(IRenderEngine* const pEngine);
bool _isInHoveredInterval(til::point coordTarget) const noexcept;
@ -131,11 +130,14 @@ namespace Microsoft::Console::Render
CursorOptions _currentCursorOptions;
std::optional<CompositionCache> _compositionCache;
std::vector<Cluster> _clusterBuffer;
std::vector<til::rect> _previousSelection;
std::function<void()> _pfnBackgroundColorChanged;
std::function<void()> _pfnFrameColorChanged;
std::function<void()> _pfnRendererEnteredErrorState;
bool _destructing = false;
bool _forceUpdateViewport = false;
til::point_span _lastSelectionPaintSpan{};
size_t _lastSelectionPaintSize{};
std::vector<til::rect> _lastSelectionRectsByViewport{};
};
}

View File

@ -27,7 +27,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT SetHwnd(const HWND hwnd) noexcept;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(std::span<const til::rect> selections) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const til::point* const pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const til::rect* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT Invalidate(const til::rect* const psrRegion) noexcept override;

View File

@ -5,6 +5,7 @@
#include "gdirenderer.hpp"
#include "../../types/inc/Viewport.hpp"
#include "../buffer/out/textBuffer.hpp"
#pragma hdrstop
@ -46,13 +47,12 @@ HRESULT GdiEngine::InvalidateScroll(const til::point* const pcoordDelta) noexcep
// - rectangles - Vector of rectangles to draw, line by line
// Return Value:
// - HRESULT S_OK or GDI-based error code
HRESULT GdiEngine::InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept
HRESULT GdiEngine::InvalidateSelection(std::span<const til::rect> selections) noexcept
{
for (const auto& rect : rectangles)
for (auto&& rect : selections)
{
RETURN_IF_FAILED(Invalidate(&rect));
}
return S_OK;
}

View File

@ -46,9 +46,9 @@ namespace Microsoft::Console::Render
virtual til::point GetTextBufferEndPosition() const noexcept = 0;
virtual TextBuffer& GetTextBuffer() const noexcept = 0;
virtual const FontInfo& GetFontInfo() const noexcept = 0;
virtual std::vector<Microsoft::Console::Types::Viewport> GetSelectionRects() noexcept = 0;
virtual std::span<const til::point_span> GetSearchHighlights() const noexcept = 0;
virtual const til::point_span* GetSearchHighlightFocused() const noexcept = 0;
virtual std::span<const til::point_span> GetSelectionSpans() const noexcept = 0;
virtual void LockConsole() noexcept = 0;
virtual void UnlockConsole() noexcept = 0;

View File

@ -32,6 +32,7 @@ namespace Microsoft::Console::Render
{
std::span<const til::point_span> searchHighlights;
const til::point_span* searchHighlightFocused;
std::span<const til::point_span> selectionSpans;
};
enum class GridLines
@ -66,7 +67,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] virtual HRESULT Invalidate(const til::rect* psrRegion) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateCursor(const til::rect* psrRegion) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateSystem(const til::rect* prcDirtyClient) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateSelection(std::span<const til::rect> selections) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateScroll(const til::point* pcoordDelta) noexcept = 0;
[[nodiscard]] virtual HRESULT InvalidateAll() noexcept = 0;

View File

@ -24,6 +24,7 @@ namespace Microsoft::Console::Render
class RenderEngineBase : public IRenderEngine
{
public:
[[nodiscard]] HRESULT InvalidateSelection(std::span<const til::rect> selections) noexcept override;
[[nodiscard]] HRESULT InvalidateHighlight(std::span<const til::point_span> highlights, const TextBuffer& buffer) noexcept override;
[[nodiscard]] HRESULT InvalidateTitle(const std::wstring_view proposedTitle) noexcept override;

View File

@ -20,7 +20,6 @@ UiaEngine::UiaEngine(IUiaEventDispatcher* dispatcher) :
_textBufferChanged{ false },
_cursorChanged{ false },
_isEnabled{ true },
_prevSelection{},
_prevCursorRegion{},
RenderEngineBase()
{
@ -110,40 +109,11 @@ CATCH_RETURN();
// - rectangles - One or more rectangles describing character positions on the grid
// Return Value:
// - S_OK
[[nodiscard]] HRESULT UiaEngine::InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept
[[nodiscard]] HRESULT UiaEngine::InvalidateSelection(std::span<const til::rect> /*rectangles*/) noexcept
{
// early exit: different number of rows
if (_prevSelection.size() != rectangles.size())
{
try
{
_selectionChanged = true;
_prevSelection = rectangles;
}
CATCH_LOG_RETURN_HR(E_FAIL);
return S_OK;
}
for (size_t i = 0; i < rectangles.size(); i++)
{
try
{
const auto prevRect = _prevSelection.at(i);
const auto newRect = rectangles.at(i);
// if any value is different, selection has changed
if (prevRect.top != newRect.top || prevRect.right != newRect.right || prevRect.left != newRect.left || prevRect.bottom != newRect.bottom)
{
_selectionChanged = true;
_prevSelection = rectangles;
return S_OK;
}
}
CATCH_LOG_RETURN_HR(E_FAIL);
}
// assume selection has not changed
_selectionChanged = false;
// INVARIANT: Renderer checks the incoming selection spans and only calls InvalidateSelection
// if they have actually changed.
_selectionChanged = true;
return S_OK;
}

View File

@ -42,7 +42,7 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT Invalidate(const til::rect* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const til::rect* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const til::rect* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(std::span<const til::rect> rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const til::point* const pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;
[[nodiscard]] HRESULT NotifyNewText(const std::wstring_view newText) noexcept override;
@ -74,7 +74,6 @@ namespace Microsoft::Console::Render
Microsoft::Console::Types::IUiaEventDispatcher* _dispatcher;
std::vector<til::rect> _prevSelection;
til::rect _prevCursorRegion;
};
}

View File

@ -181,11 +181,6 @@ CATCH_RETURN()
return S_OK;
}
[[nodiscard]] HRESULT WddmConEngine::InvalidateSelection(const std::vector<til::rect>& /*rectangles*/) noexcept
{
return S_OK;
}
[[nodiscard]] HRESULT WddmConEngine::InvalidateScroll(const til::point* const /*pcoordDelta*/) noexcept
{
return S_OK;

View File

@ -28,7 +28,6 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT Invalidate(const til::rect* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateCursor(const til::rect* const psrRegion) noexcept override;
[[nodiscard]] HRESULT InvalidateSystem(const til::rect* const prcDirtyClient) noexcept override;
[[nodiscard]] HRESULT InvalidateSelection(const std::vector<til::rect>& rectangles) noexcept override;
[[nodiscard]] HRESULT InvalidateScroll(const til::point* const pcoordDelta) noexcept override;
[[nodiscard]] HRESULT InvalidateAll() noexcept override;