// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "UiaTextRangeBase.hpp" #include "ScreenInfoUiaProviderBase.h" #include "../buffer/out/search.h" #include "UiaTracing.h" using namespace Microsoft::Console::Types; // Foreground/Background text color doesn't care about the alpha. static constexpr long _RemoveAlpha(COLORREF color) noexcept { return color & 0x00ffffff; } // degenerate range constructor. #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ std::wstring_view wordDelimiters) noexcept try { RETURN_HR_IF_NULL(E_INVALIDARG, pProvider); RETURN_HR_IF_NULL(E_INVALIDARG, pData); _pProvider = pProvider; _pData = pData; _start = pData->GetViewport().Origin(); _end = pData->GetViewport().Origin(); _blockRange = false; _wordDelimiters = wordDelimiters; UiaTracing::TextRange::Constructor(*this); return S_OK; } CATCH_RETURN(); #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const Cursor& cursor, _In_ std::wstring_view wordDelimiters) noexcept try { RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters)); _start = cursor.GetPosition(); _end = _start; UiaTracing::TextRange::Constructor(*this); return S_OK; } CATCH_RETURN(); #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ IUiaData* pData, _In_ IRawElementProviderSimple* const pProvider, _In_ const COORD start, _In_ const COORD end, _In_ bool blockRange, _In_ std::wstring_view wordDelimiters) noexcept try { RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters)); // start is before/at end, so this is valid if (_pData->GetTextBuffer().GetSize().CompareInBounds(start, end, true) <= 0) { _start = start; _end = end; } else { // start is after end, so we need to flip our concept of start/end _start = end; _end = start; } // This should be the only way to set if we are a blockRange // This is used for blockSelection _blockRange = blockRange; UiaTracing::TextRange::Constructor(*this); return S_OK; } CATCH_RETURN(); void UiaTextRangeBase::Initialize(_In_ const UiaPoint point) { POINT clientPoint; clientPoint.x = static_cast(point.x); clientPoint.y = static_cast(point.y); // get row that point resides in const RECT windowRect = _getTerminalRect(); const SMALL_RECT viewport = _pData->GetViewport().ToInclusive(); short row = 0; if (clientPoint.y <= windowRect.top) { row = viewport.Top; } else if (clientPoint.y >= windowRect.bottom) { row = viewport.Bottom; } else { // change point coords to pixels relative to window _TranslatePointFromScreen(&clientPoint); const COORD currentFontSize = _getScreenFontSize(); row = gsl::narrow(clientPoint.y / static_cast(currentFontSize.Y)) + viewport.Top; } _start = { 0, row }; _end = _start; } #pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize HRESULT UiaTextRangeBase::RuntimeClassInitialize(const UiaTextRangeBase& a) noexcept try { _pProvider = a._pProvider; _start = a._start; _end = a._end; _pData = a._pData; _wordDelimiters = a._wordDelimiters; _blockRange = a._blockRange; UiaTracing::TextRange::Constructor(*this); return S_OK; } CATCH_RETURN(); const COORD UiaTextRangeBase::GetEndpoint(TextPatternRangeEndpoint endpoint) const noexcept { switch (endpoint) { case TextPatternRangeEndpoint_End: return _end; case TextPatternRangeEndpoint_Start: default: return _start; } } // Routine Description: // - sets the target endpoint to the given COORD value // - if the target endpoint crosses the other endpoint, become a degenerate range // Arguments: // - endpoint - the target endpoint (start or end) // - val - the value that it will be set to // Return Value: // - true if range is degenerate, false otherwise. bool UiaTextRangeBase::SetEndpoint(TextPatternRangeEndpoint endpoint, const COORD val) noexcept { // GH#6402: Get the actual buffer size here, instead of the one // constrained by the virtual bottom. const auto bufferSize = _pData->GetTextBuffer().GetSize(); switch (endpoint) { case TextPatternRangeEndpoint_End: _end = val; // if end is before start... if (bufferSize.CompareInBounds(_end, _start, true) < 0) { // make this range degenerate at end _start = _end; } break; case TextPatternRangeEndpoint_Start: _start = val; // if start is after end... if (bufferSize.CompareInBounds(_start, _end, true) > 0) { // make this range degenerate at start _end = _start; } break; default: break; } return IsDegenerate(); } // Routine Description: // - returns true if the range is currently degenerate (empty range). // Arguments: // - // Return Value: // - true if range is degenerate, false otherwise. const bool UiaTextRangeBase::IsDegenerate() const noexcept { return _start == _end; } #pragma region ITextRangeProvider IFACEMETHODIMP UiaTextRangeBase::Compare(_In_opt_ ITextRangeProvider* pRange, _Out_ BOOL* pRetVal) noexcept { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = FALSE; const UiaTextRangeBase* other = static_cast(pRange); if (other) { *pRetVal = (_start == other->GetEndpoint(TextPatternRangeEndpoint_Start) && _end == other->GetEndpoint(TextPatternRangeEndpoint_End)); } UiaTracing::TextRange::Compare(*this, *other, *pRetVal); return S_OK; } IFACEMETHODIMP UiaTextRangeBase::CompareEndpoints(_In_ TextPatternRangeEndpoint endpoint, _In_ ITextRangeProvider* pTargetRange, _In_ TextPatternRangeEndpoint targetEndpoint, _Out_ int* pRetVal) noexcept try { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; // get the text range that we're comparing to const UiaTextRangeBase* range = static_cast(pTargetRange); if (range == nullptr) { return E_INVALIDARG; } // get endpoint value that we're comparing to const auto other = range->GetEndpoint(targetEndpoint); // get the values of our endpoint const auto mine = GetEndpoint(endpoint); // TODO GH#5406: create a different UIA parent object for each TextBuffer // This is a temporary solution to comparing two UTRs from different TextBuffers // Ensure both endpoints fit in the current buffer. const auto bufferSize = _pData->GetTextBuffer().GetSize(); if (!bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true)) { return E_FAIL; } // compare them *pRetVal = bufferSize.CompareInBounds(mine, other, true); UiaTracing::TextRange::CompareEndpoints(*this, endpoint, *range, targetEndpoint, *pRetVal); return S_OK; } CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::ExpandToEnclosingUnit(_In_ TextUnit unit) noexcept { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); try { _expandToEnclosingUnit(unit); UiaTracing::TextRange::ExpandToEnclosingUnit(unit, *this); return S_OK; } CATCH_RETURN(); } // Method Description: // - Moves _start and _end endpoints to encompass the enclosing text unit. // (i.e. word --> enclosing word, line --> enclosing line) // - IMPORTANT: this does _not_ lock the console // Arguments: // - attributeId - the UIA text attribute identifier we're expanding by // Return Value: // - void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit) { const auto& buffer = _pData->GetTextBuffer(); const auto bufferSize = _getBufferSize(); const auto bufferEnd = bufferSize.EndExclusive(); if (unit == TextUnit_Character) { _start = buffer.GetGlyphStart(_start); _end = buffer.GetGlyphEnd(_start); } else if (unit <= TextUnit_Word) { // expand to word _start = buffer.GetWordStart(_start, _wordDelimiters, true); _end = buffer.GetWordEnd(_start, _wordDelimiters, true); // GetWordEnd may return the actual end of the TextBuffer. // If so, just set it to this value of bufferEnd if (!bufferSize.IsInBounds(_end)) { _end = bufferEnd; } } else if (unit <= TextUnit_Line) { if (_start == bufferEnd) { // Special case: if we are at the bufferEnd, // move _start back one, instead of _end forward _start.X = 0; _start.Y = base::ClampSub(_start.Y, 1); _end = bufferEnd; } else { // expand to line _start.X = 0; _end.X = 0; _end.Y = base::ClampAdd(_start.Y, 1); } } else { // TODO GH#6986: properly handle "end of buffer" as last character // instead of last cell // expand to document _start = bufferSize.Origin(); _end = bufferSize.EndExclusive(); } } // Method Description: // - Verify that the given attribute has the desired formatting saved in the attributeId and val // Arguments: // - attributeId - the UIA text attribute identifier we're looking for // - val - the attributeId's sub-type we're looking for // - attr - the text attribute we're checking // Return Value: // - true, if the given attribute has the desired formatting. // - false, if the given attribute does not have the desired formatting. // - nullopt, if checking for the desired formatting is not supported. std::optional UiaTextRangeBase::_verifyAttr(TEXTATTRIBUTEID attributeId, VARIANT val, const TextAttribute& attr) const { // Most of the attributes we're looking for just require us to check TextAttribute. // So if we support it, we'll return a function to verify if the TextAttribute // has the desired attribute. switch (attributeId) { case UIA_BackgroundColorAttributeId: { // Expected type: VT_I4 THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); // The foreground color is stored as a COLORREF. const auto queryBackgroundColor{ val.lVal }; return _RemoveAlpha(_pData->GetAttributeColors(attr).second) == queryBackgroundColor; } case UIA_FontWeightAttributeId: { // Expected type: VT_I4 THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); // The font weight can be any value from 0 to 900. // The text buffer doesn't store the actual value, // we just store "IsBold" and "IsFaint". const auto queryFontWeight{ val.lVal }; if (queryFontWeight > FW_NORMAL) { // we're looking for a bold font weight return attr.IsBold(); } else { // we're looking for "normal" font weight return !attr.IsBold(); } } case UIA_ForegroundColorAttributeId: { // Expected type: VT_I4 THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); // The foreground color is stored as a COLORREF. const auto queryForegroundColor{ val.lVal }; return _RemoveAlpha(_pData->GetAttributeColors(attr).first) == queryForegroundColor; } case UIA_IsItalicAttributeId: { // Expected type: VT_I4 THROW_HR_IF(E_INVALIDARG, val.vt != VT_BOOL); // The text is either italic or it isn't. const auto queryIsItalic{ val.boolVal }; return queryIsItalic ? attr.IsItalic() : !attr.IsItalic(); } case UIA_StrikethroughStyleAttributeId: { // Expected type: VT_I4 THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); // The strikethrough style is stored as a TextDecorationLineStyle. // However, The text buffer doesn't have different styles for being crossed out. // Instead, we just store whether or not the text is crossed out. switch (val.lVal) { case TextDecorationLineStyle_None: return !attr.IsCrossedOut(); case TextDecorationLineStyle_Single: return attr.IsCrossedOut(); default: return std::nullopt; } } case UIA_UnderlineStyleAttributeId: { // Expected type: VT_I4 THROW_HR_IF(E_INVALIDARG, val.vt != VT_I4); // The underline style is stored as a TextDecorationLineStyle. // However, The text buffer doesn't have that many different styles for being underlined. // Instead, we only have single and double underlined. switch (val.lVal) { case TextDecorationLineStyle_None: return !attr.IsUnderlined() && !attr.IsDoublyUnderlined(); case TextDecorationLineStyle_Double: return attr.IsDoublyUnderlined(); case TextDecorationLineStyle_Single: return attr.IsUnderlined(); default: return std::nullopt; } } default: return std::nullopt; } } IFACEMETHODIMP UiaTextRangeBase::FindAttribute(_In_ TEXTATTRIBUTEID attributeId, _In_ VARIANT val, _In_ BOOL searchBackwards, _Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) noexcept try { RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); *ppRetVal = nullptr; // AttributeIDs that require special handling switch (attributeId) { case UIA_FontNameAttributeId: { RETURN_HR_IF(E_INVALIDARG, val.vt != VT_BSTR); // Technically, we'll truncate early if there's an embedded null in the BSTR. // But we're probably fine in this circumstance. const std::wstring queryFontName{ val.bstrVal }; if (queryFontName == _pData->GetFontInfo().GetFaceName()) { Clone(ppRetVal); } UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal)); return S_OK; } case UIA_IsReadOnlyAttributeId: { RETURN_HR_IF(E_INVALIDARG, val.vt != VT_BOOL); if (!val.boolVal) { Clone(ppRetVal); } UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal)); return S_OK; } default: break; } // AttributeIDs that are exposed via TextAttribute try { if (!_verifyAttr(attributeId, val, {}).has_value()) { // The AttributeID is not supported. UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal), UiaTracing::AttributeType::Unsupported); return E_NOTIMPL; } } catch (...) { LOG_HR(wil::ResultFromCaughtException()); UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal), UiaTracing::AttributeType::Error); return E_INVALIDARG; } // Get some useful variables const auto& buffer{ _pData->GetTextBuffer() }; const auto bufferSize{ buffer.GetSize() }; const auto inclusiveEnd{ _getInclusiveEnd() }; // Start/End for the resulting range. // NOTE: we store these as "first" and "second" anchor because, // we just want to know what the inclusive range is. // We'll do some post-processing to fix this on the way out. std::optional resultFirstAnchor; std::optional resultSecondAnchor; const auto attemptUpdateAnchors = [=, &resultFirstAnchor, &resultSecondAnchor](const TextBufferCellIterator iter) { const auto attrFound{ _verifyAttr(attributeId, val, iter->TextAttr()).value() }; if (attrFound) { // populate the first anchor if it's not populated. // otherwise, populate the second anchor. if (!resultFirstAnchor.has_value()) { resultFirstAnchor = iter.Pos(); resultSecondAnchor = iter.Pos(); } else { resultSecondAnchor = iter.Pos(); } } return attrFound; }; // Start/End for the direction to perform the search in // We need searchEnd to be exclusive. This allows the for-loop below to // iterate up until the exclusive searchEnd, and not attempt to read the // data at that position. const auto searchStart{ searchBackwards ? inclusiveEnd : _start }; const auto searchEndInclusive{ searchBackwards ? _start : inclusiveEnd }; auto searchEndExclusive{ searchEndInclusive }; if (searchBackwards) { bufferSize.DecrementInBounds(searchEndExclusive, true); } else { bufferSize.IncrementInBounds(searchEndExclusive, true); } // Iterate from searchStart to searchEnd in the buffer. // If we find the attribute we're looking for, we update resultFirstAnchor/SecondAnchor appropriately. Viewport viewportRange{ bufferSize }; if (_blockRange) { const auto originX{ std::min(_start.X, inclusiveEnd.X) }; const auto originY{ std::min(_start.Y, inclusiveEnd.Y) }; const auto width{ gsl::narrow_cast(std::abs(inclusiveEnd.X - _start.X + 1)) }; const auto height{ gsl::narrow_cast(std::abs(inclusiveEnd.Y - _start.Y + 1)) }; viewportRange = Viewport::FromDimensions({ originX, originY }, width, height); } auto iter{ buffer.GetCellDataAt(searchStart, viewportRange) }; const auto iterStep{ searchBackwards ? -1 : 1 }; for (; iter && iter.Pos() != searchEndExclusive; iter += iterStep) { if (!attemptUpdateAnchors(iter) && resultFirstAnchor.has_value() && resultSecondAnchor.has_value()) { // Exit the loop early if... // - the cell we're looking at doesn't have the attr we're looking for // - the anchors have been populated // This means that we've found a contiguous range where the text attribute was found. // No point in searching through the rest of the search space. // TLDR: keep updating the second anchor and make the range wider until the attribute changes. break; } } // Corner case: we couldn't actually move the searchEnd to make it exclusive // (i.e. DecrementInBounds on Origin doesn't move it) if (searchEndInclusive == searchEndExclusive) { attemptUpdateAnchors(iter); } // If a result was found, populate ppRetVal with the UiaTextRange // representing the found selection anchors. if (resultFirstAnchor.has_value() && resultSecondAnchor.has_value()) { RETURN_IF_FAILED(Clone(ppRetVal)); UiaTextRangeBase& range = static_cast(**ppRetVal); // IMPORTANT: resultFirstAnchor and resultSecondAnchor make up an inclusive range. range._start = searchBackwards ? *resultSecondAnchor : *resultFirstAnchor; range._end = searchBackwards ? *resultFirstAnchor : *resultSecondAnchor; // We need to make the end exclusive! // But be careful here, we might be a block range auto exclusiveIter{ buffer.GetCellDataAt(range._end, viewportRange) }; ++exclusiveIter; range._end = exclusiveIter.Pos(); } UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast(**ppRetVal)); return S_OK; } CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::FindText(_In_ BSTR text, _In_ BOOL searchBackward, _In_ BOOL ignoreCase, _Outptr_result_maybenull_ ITextRangeProvider** ppRetVal) noexcept try { RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); *ppRetVal = nullptr; const std::wstring queryText{ text, SysStringLen(text) }; const auto bufferSize = _getBufferSize(); const auto sensitivity = ignoreCase ? Search::Sensitivity::CaseInsensitive : Search::Sensitivity::CaseSensitive; auto searchDirection = Search::Direction::Forward; auto searchAnchor = _start; if (searchBackward) { searchDirection = Search::Direction::Backward; // we need to convert the end to inclusive // because Search operates with an inclusive COORD searchAnchor = _end; bufferSize.DecrementInBounds(searchAnchor, true); } Search searcher{ *_pData, queryText, searchDirection, sensitivity, searchAnchor }; if (searcher.FindNext()) { const auto foundLocation = searcher.GetFoundLocation(); const auto start = foundLocation.first; // we need to increment the position of end because it's exclusive auto end = foundLocation.second; bufferSize.IncrementInBounds(end, true); // make sure what was found is within the bounds of the current range if ((searchDirection == Search::Direction::Forward && bufferSize.CompareInBounds(end, _end, true) < 0) || (searchDirection == Search::Direction::Backward && bufferSize.CompareInBounds(start, _start) > 0)) { RETURN_IF_FAILED(Clone(ppRetVal)); UiaTextRangeBase& range = static_cast(**ppRetVal); range._start = start; range._end = end; UiaTracing::TextRange::FindText(*this, queryText, searchBackward, ignoreCase, range); } } return S_OK; } CATCH_RETURN(); // Method Description: // - (1) Checks the current range for the attributeId's sub-type // - (2) Record the attributeId's sub-type // Arguments: // - attributeId - the UIA text attribute identifier we're looking for // - pRetVal - the attributeId's sub-type for the first cell in the range (i.e. foreground color) // - attr - the text attribute we're checking // Return Value: // - true, if the attributeId is supported. false, otherwise. // - pRetVal is populated with the appropriate response relevant to the returned bool. bool UiaTextRangeBase::_initializeAttrQuery(TEXTATTRIBUTEID attributeId, VARIANT* pRetVal, const TextAttribute& attr) const { THROW_HR_IF(E_INVALIDARG, pRetVal == nullptr); switch (attributeId) { case UIA_BackgroundColorAttributeId: { pRetVal->vt = VT_I4; pRetVal->lVal = _RemoveAlpha(_pData->GetAttributeColors(attr).second); return true; } case UIA_FontWeightAttributeId: { // The font weight can be any value from 0 to 900. // The text buffer doesn't store the actual value, // we just store "IsBold" and "IsFaint". // Source: https://docs.microsoft.com/en-us/windows/win32/winauto/uiauto-textattribute-ids pRetVal->vt = VT_I4; pRetVal->lVal = attr.IsBold() ? FW_BOLD : FW_NORMAL; return true; } case UIA_ForegroundColorAttributeId: { pRetVal->vt = VT_I4; pRetVal->lVal = _RemoveAlpha(_pData->GetAttributeColors(attr).first); return true; } case UIA_IsItalicAttributeId: { pRetVal->vt = VT_BOOL; pRetVal->boolVal = attr.IsItalic(); return true; } case UIA_StrikethroughStyleAttributeId: { pRetVal->vt = VT_I4; pRetVal->lVal = attr.IsCrossedOut() ? TextDecorationLineStyle_Single : TextDecorationLineStyle_None; return true; } case UIA_UnderlineStyleAttributeId: { pRetVal->vt = VT_I4; if (attr.IsDoublyUnderlined()) { pRetVal->lVal = TextDecorationLineStyle_Double; } else if (attr.IsUnderlined()) { pRetVal->lVal = TextDecorationLineStyle_Single; } else { pRetVal->lVal = TextDecorationLineStyle_None; } return true; } default: // This attribute is not supported. pRetVal->vt = VT_UNKNOWN; UiaGetReservedNotSupportedValue(&pRetVal->punkVal); return false; } } IFACEMETHODIMP UiaTextRangeBase::GetAttributeValue(_In_ TEXTATTRIBUTEID attributeId, _Out_ VARIANT* pRetVal) noexcept try { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); VariantInit(pRetVal); // AttributeIDs that require special handling switch (attributeId) { case UIA_FontNameAttributeId: { pRetVal->vt = VT_BSTR; pRetVal->bstrVal = SysAllocString(_pData->GetFontInfo().GetFaceName().data()); UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal); return S_OK; } case UIA_IsReadOnlyAttributeId: { pRetVal->vt = VT_BOOL; pRetVal->boolVal = VARIANT_FALSE; UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal); return S_OK; } default: break; } // AttributeIDs that are exposed via TextAttribute try { // Unlike a normal text editor, which applies formatting at the caret, // we don't know what attributes are written at a degenerate range. // So instead, we'll use GetCurrentAttributes to get an idea of the default // text attributes used. And return a result based off of that. const auto attr{ IsDegenerate() ? _pData->GetTextBuffer().GetCurrentAttributes() : _pData->GetTextBuffer().GetCellDataAt(_start)->TextAttr() }; if (!_initializeAttrQuery(attributeId, pRetVal, attr)) { // The AttributeID is not supported. pRetVal->vt = VT_UNKNOWN; UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal, UiaTracing::AttributeType::Unsupported); return UiaGetReservedNotSupportedValue(&pRetVal->punkVal); } else if (IsDegenerate()) { // If we're a degenerate range, we have all the information we need. UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal); return S_OK; } } catch (...) { LOG_HR(wil::ResultFromCaughtException()); UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal, UiaTracing::AttributeType::Error); return E_INVALIDARG; } // Get some useful variables const auto& buffer{ _pData->GetTextBuffer() }; const auto bufferSize{ buffer.GetSize() }; const auto inclusiveEnd{ _getInclusiveEnd() }; // Check if the entire text range has that text attribute Viewport viewportRange{ bufferSize }; if (_blockRange) { const auto originX{ std::min(_start.X, inclusiveEnd.X) }; const auto originY{ std::min(_start.Y, inclusiveEnd.Y) }; const auto width{ gsl::narrow_cast(std::abs(inclusiveEnd.X - _start.X + 1)) }; const auto height{ gsl::narrow_cast(std::abs(inclusiveEnd.Y - _start.Y + 1)) }; viewportRange = Viewport::FromDimensions({ originX, originY }, width, height); } auto iter{ buffer.GetCellDataAt(_start, viewportRange) }; for (; iter && iter.Pos() != inclusiveEnd; ++iter) { if (!_verifyAttr(attributeId, *pRetVal, iter->TextAttr()).value()) { // The value of the specified attribute varies over the text range // return UiaGetReservedMixedAttributeValue. // Source: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationcore/nf-uiautomationcore-itextrangeprovider-getattributevalue pRetVal->vt = VT_UNKNOWN; UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal, UiaTracing::AttributeType::Mixed); return UiaGetReservedMixedAttributeValue(&pRetVal->punkVal); } } UiaTracing::TextRange::GetAttributeValue(*this, attributeId, *pRetVal); return S_OK; } CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::GetBoundingRectangles(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); *ppRetVal = nullptr; try { // vector to put coords into. they go in as four doubles in the // order: left, top, width, height. each line will have its own // set of coords. std::vector coords; // GH#6402: Get the actual buffer size here, instead of the one // constrained by the virtual bottom. const auto& buffer = _pData->GetTextBuffer(); const auto bufferSize = buffer.GetSize(); // these viewport vars are converted to the buffer coordinate space const auto viewport = bufferSize.ConvertToOrigin(_pData->GetViewport()); const til::point viewportOrigin = viewport.Origin(); const auto viewportEnd = viewport.EndExclusive(); // startAnchor: the earliest COORD we will get a bounding rect for auto startAnchor = GetEndpoint(TextPatternRangeEndpoint_Start); if (bufferSize.CompareInBounds(startAnchor, viewportOrigin, true) < 0) { // earliest we can be is the origin startAnchor = viewportOrigin; } // endAnchor: the latest COORD we will get a bounding rect for auto endAnchor = GetEndpoint(TextPatternRangeEndpoint_End); if (bufferSize.CompareInBounds(endAnchor, viewportEnd, true) > 0) { // latest we can be is the viewport end endAnchor = viewportEnd; } // _end is exclusive, let's be inclusive so we don't have to think about it anymore for bounding rects bufferSize.DecrementInBounds(endAnchor, true); if (IsDegenerate() || bufferSize.CompareInBounds(_start, viewportEnd, true) > 0 || bufferSize.CompareInBounds(_end, viewportOrigin, true) < 0) { // An empty array is returned for a degenerate (empty) text range. // reference: https://docs.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationtextrange-getboundingrectangles // Remember, start cannot be past end, so // if start is past the viewport end, // or end is past the viewport origin // draw nothing } else { const auto textRects = buffer.GetTextRects(startAnchor, endAnchor, _blockRange, true); for (const auto& rect : textRects) { // Convert the buffer coordinates to an equivalent range of // screen cells, taking line rendition into account. const auto lineRendition = buffer.GetLineRendition(rect.Top); til::rectangle r{ BufferToScreenLine(rect, lineRendition) }; r -= viewportOrigin; _getBoundingRect(r, coords); } } // convert to a safearray *ppRetVal = SafeArrayCreateVector(VT_R8, 0, gsl::narrow(coords.size())); if (*ppRetVal == nullptr) { return E_OUTOFMEMORY; } HRESULT hr = E_UNEXPECTED; for (LONG i = 0; i < gsl::narrow(coords.size()); ++i) { hr = SafeArrayPutElement(*ppRetVal, &i, &coords.at(i)); if (FAILED(hr)) { SafeArrayDestroy(*ppRetVal); *ppRetVal = nullptr; return hr; } } } CATCH_RETURN(); UiaTracing::TextRange::GetBoundingRectangles(*this); return S_OK; } IFACEMETHODIMP UiaTextRangeBase::GetEnclosingElement(_Outptr_result_maybenull_ IRawElementProviderSimple** ppRetVal) noexcept try { RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); *ppRetVal = nullptr; const auto hr = _pProvider->QueryInterface(IID_PPV_ARGS(ppRetVal)); UiaTracing::TextRange::GetEnclosingElement(*this); return hr; } CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::GetText(_In_ int maxLength, _Out_ BSTR* pRetVal) noexcept try { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = nullptr; if (maxLength < -1) { return E_INVALIDARG; } const auto maxLengthOpt = (maxLength == -1) ? std::nullopt : std::optional{ maxLength }; _pData->LockConsole(); auto Unlock = wil::scope_exit([this]() noexcept { _pData->UnlockConsole(); }); const auto text = _getTextValue(maxLengthOpt); Unlock.reset(); *pRetVal = SysAllocString(text.c_str()); RETURN_HR_IF_NULL(E_OUTOFMEMORY, *pRetVal); UiaTracing::TextRange::GetText(*this, maxLength, text); return S_OK; } CATCH_RETURN(); // Method Description: // - Helper method for GetText(). Retrieves the text that the UiaTextRange encompasses as a wstring // Arguments: // - maxLength - the maximum size of the retrieved text. nullopt means we don't care about the size. // Return Value: // - the text that the UiaTextRange encompasses #pragma warning(push) #pragma warning(disable : 26447) // compiler isn't filtering throws inside the try/catch std::wstring UiaTextRangeBase::_getTextValue(std::optional maxLength) const { std::wstring textData{}; if (!IsDegenerate()) { const auto& buffer = _pData->GetTextBuffer(); const auto bufferSize = buffer.GetSize(); // TODO GH#5406: create a different UIA parent object for each TextBuffer // nvaccess/nvda#11428: Ensure our endpoints are in bounds // otherwise, we'll FailFast catastrophically if (!bufferSize.IsInBounds(_start, true) || !bufferSize.IsInBounds(_end, true)) { THROW_HR(E_FAIL); } // convert _end to be inclusive auto inclusiveEnd = _end; bufferSize.DecrementInBounds(inclusiveEnd, true); const auto textRects = buffer.GetTextRects(_start, inclusiveEnd, _blockRange, true); const auto bufferData = buffer.GetText(true, false, textRects); const size_t textDataSize = base::ClampMul(bufferData.text.size(), bufferSize.Width()); textData.reserve(textDataSize); for (const auto& text : bufferData.text) { textData += text; } } if (maxLength.has_value()) { textData.resize(*maxLength); } return textData; } #pragma warning(pop) IFACEMETHODIMP UiaTextRangeBase::Move(_In_ TextUnit unit, _In_ int count, _Out_ int* pRetVal) noexcept try { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); const auto wasDegenerate = IsDegenerate(); if (count != 0) { // We can abstract this movement by moving _start constexpr auto endpoint = TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start; const auto preventBoundary = !wasDegenerate; if (unit == TextUnit::TextUnit_Character) { _moveEndpointByUnitCharacter(count, endpoint, pRetVal, preventBoundary); } else if (unit <= TextUnit::TextUnit_Word) { // TODO GH#10925: passing in "true" instead of "preventBoundary" // We still need to go through the process of writing // tests, finding failing cases, and fixing them. // For now, just use true because we've been doing that so far. // The tests at the time of writing don't report any failures // if we use one over the other. _moveEndpointByUnitWord(count, endpoint, pRetVal, true); } else if (unit <= TextUnit::TextUnit_Line) { _moveEndpointByUnitLine(count, endpoint, pRetVal, preventBoundary); } else if (unit <= TextUnit::TextUnit_Document) { _moveEndpointByUnitDocument(count, endpoint, pRetVal, preventBoundary); } } if (wasDegenerate) { // GH#7342: The range was degenerate before the move. // To keep it that way, move _end to the new _start. _end = _start; } else { // then just expand to get our _end _expandToEnclosingUnit(unit); } UiaTracing::TextRange::Move(unit, count, *pRetVal, *this); return S_OK; } CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByUnit(_In_ TextPatternRangeEndpoint endpoint, _In_ TextUnit unit, _In_ int count, _Out_ int* pRetVal) noexcept { RETURN_HR_IF(E_INVALIDARG, pRetVal == nullptr); *pRetVal = 0; if (count == 0) { return S_OK; } _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); try { if (unit == TextUnit::TextUnit_Character) { _moveEndpointByUnitCharacter(count, endpoint, pRetVal); } else if (unit <= TextUnit::TextUnit_Word) { _moveEndpointByUnitWord(count, endpoint, pRetVal); } else if (unit <= TextUnit::TextUnit_Line) { _moveEndpointByUnitLine(count, endpoint, pRetVal); } else if (unit <= TextUnit::TextUnit_Document) { _moveEndpointByUnitDocument(count, endpoint, pRetVal); } } CATCH_RETURN(); UiaTracing::TextRange::MoveEndpointByUnit(endpoint, unit, count, *pRetVal, *this); return S_OK; } IFACEMETHODIMP UiaTextRangeBase::MoveEndpointByRange(_In_ TextPatternRangeEndpoint endpoint, _In_ ITextRangeProvider* pTargetRange, _In_ TextPatternRangeEndpoint targetEndpoint) noexcept try { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); const UiaTextRangeBase* range = static_cast(pTargetRange); if (range == nullptr) { return E_INVALIDARG; } // TODO GH#5406: create a different UIA parent object for each TextBuffer // This is a temporary solution to comparing two UTRs from different TextBuffers // Ensure both endpoints fit in the current buffer. const auto bufferSize = _pData->GetTextBuffer().GetSize(); const auto mine = GetEndpoint(endpoint); const auto other = range->GetEndpoint(targetEndpoint); if (!bufferSize.IsInBounds(mine, true) || !bufferSize.IsInBounds(other, true)) { return E_FAIL; } SetEndpoint(endpoint, range->GetEndpoint(targetEndpoint)); UiaTracing::TextRange::MoveEndpointByRange(endpoint, *range, targetEndpoint, *this); return S_OK; } CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::Select() noexcept try { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); if (IsDegenerate()) { // calling Select on a degenerate range should clear any current selections _pData->ClearSelection(); } else { const auto bufferSize = _pData->GetTextBuffer().GetSize(); if (!bufferSize.IsInBounds(_start, true) || !bufferSize.IsInBounds(_end, true)) { return E_FAIL; } auto inclusiveEnd = _end; bufferSize.DecrementInBounds(inclusiveEnd); _pData->SelectNewRegion(_start, inclusiveEnd); } UiaTracing::TextRange::Select(*this); return S_OK; } CATCH_RETURN(); // we don't support this IFACEMETHODIMP UiaTextRangeBase::AddToSelection() noexcept { UiaTracing::TextRange::AddToSelection(*this); return E_NOTIMPL; } // we don't support this IFACEMETHODIMP UiaTextRangeBase::RemoveFromSelection() noexcept { UiaTracing::TextRange::RemoveFromSelection(*this); return E_NOTIMPL; } IFACEMETHODIMP UiaTextRangeBase::ScrollIntoView(_In_ BOOL alignToTop) noexcept try { _pData->LockConsole(); auto Unlock = wil::scope_exit([&]() noexcept { _pData->UnlockConsole(); }); const auto oldViewport = _pData->GetViewport().ToInclusive(); const auto viewportHeight = _getViewportHeight(oldViewport); // range rows const base::ClampedNumeric startScreenInfoRow = _start.Y; const base::ClampedNumeric endScreenInfoRow = _end.Y; // screen buffer rows const base::ClampedNumeric topRow = 0; const base::ClampedNumeric bottomRow = _pData->GetTextBuffer().TotalRowCount() - 1; SMALL_RECT newViewport = oldViewport; // there's a bunch of +1/-1s here for setting the viewport. These // are to account for the inclusivity of the viewport boundaries. if (alignToTop) { // determine if we can align the start row to the top if (startScreenInfoRow + viewportHeight <= bottomRow) { // we can align to the top newViewport.Top = startScreenInfoRow; newViewport.Bottom = startScreenInfoRow + viewportHeight - 1; } else { // we can't align to the top so we'll just move the viewport // to the bottom of the screen buffer newViewport.Bottom = bottomRow; newViewport.Top = bottomRow - viewportHeight + 1; } } else { // we need to align to the bottom // check if we can align to the bottom if (static_cast(endScreenInfoRow) >= viewportHeight) { // GH#7839: endScreenInfoRow may be ExclusiveEnd // ExclusiveEnd is past the bottomRow // so we need to clamp to the bottom row to stay in bounds // we can align to bottom newViewport.Bottom = std::min(endScreenInfoRow, bottomRow); newViewport.Top = base::ClampedNumeric(newViewport.Bottom) - viewportHeight + 1; } else { // we can't align to bottom so we'll move the viewport to // the top of the screen buffer newViewport.Top = topRow; newViewport.Bottom = topRow + viewportHeight - 1; } } FAIL_FAST_IF(!(newViewport.Top >= topRow)); FAIL_FAST_IF(!(newViewport.Bottom <= bottomRow)); FAIL_FAST_IF(!(_getViewportHeight(oldViewport) == _getViewportHeight(newViewport))); Unlock.reset(); const gsl::not_null provider = static_cast(_pProvider); provider->ChangeViewport(newViewport); UiaTracing::TextRange::ScrollIntoView(alignToTop, *this); return S_OK; } CATCH_RETURN(); IFACEMETHODIMP UiaTextRangeBase::GetChildren(_Outptr_result_maybenull_ SAFEARRAY** ppRetVal) noexcept { RETURN_HR_IF(E_INVALIDARG, ppRetVal == nullptr); // we don't have any children *ppRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, 0); if (*ppRetVal == nullptr) { return E_OUTOFMEMORY; } UiaTracing::TextRange::GetChildren(*this); return S_OK; } #pragma endregion const COORD UiaTextRangeBase::_getScreenFontSize() const { COORD coordRet = _pData->GetFontInfo().GetSize(); // For sanity's sake, make sure not to leak 0 out as a possible value. These values are used in division operations. coordRet.X = std::max(coordRet.X, 1i16); coordRet.Y = std::max(coordRet.Y, 1i16); return coordRet; } // Routine Description: // - Gets the viewport height, measured in char rows. // Arguments: // - viewport - The viewport to measure // Return Value: // - The viewport height const unsigned int UiaTextRangeBase::_getViewportHeight(const SMALL_RECT viewport) const noexcept { FAIL_FAST_IF(!(viewport.Bottom >= viewport.Top)); // + 1 because COORD is inclusive on both sides so subtracting top // and bottom gets rid of 1 more then it should. return viewport.Bottom - viewport.Top + 1; } // Routine Description: // - Gets a viewport representing where valid text may be in the TextBuffer // - Use this instead of `textBuffer.GetSize()`. This improves performance // because it's a smaller space to have to search through // Arguments: // - // Return Value: // - A viewport representing the portion of the TextBuffer that has valid text const Viewport UiaTextRangeBase::_getBufferSize() const noexcept { // we need to add 1 to the X/Y of textBufferEnd // because we want the returned viewport to include this COORD const auto textBufferEnd = _pData->GetTextBufferEndPosition(); const auto width = base::ClampAdd(1, textBufferEnd.X); const auto height = base::ClampAdd(1, textBufferEnd.Y); return Viewport::FromDimensions({ 0, 0 }, width, height); } // Routine Description: // - adds the relevant coordinate points from the row to coords. // - it is assumed that startAnchor and endAnchor are within the same row // and NOT DEGENERATE // Arguments: // - startAnchor - the start anchor of interested data within the viewport. In text buffer coordinate space. Inclusive. // - endAnchor - the end anchor of interested data within the viewport. In text buffer coordinate space. Inclusive // - coords - vector to add the calculated coords to // Return Value: // - void UiaTextRangeBase::_getBoundingRect(const til::rectangle textRect, _Inout_ std::vector& coords) const { const til::size currentFontSize = _getScreenFontSize(); POINT topLeft{ 0 }; POINT bottomRight{ 0 }; // we want to clamp to a long (output type), not a short (input type) // so we need to explicitly say topLeft.x = base::ClampMul(textRect.left(), currentFontSize.width()); topLeft.y = base::ClampMul(textRect.top(), currentFontSize.height()); bottomRight.x = base::ClampMul(textRect.right(), currentFontSize.width()); bottomRight.y = base::ClampMul(textRect.bottom(), currentFontSize.height()); // convert the coords to be relative to the screen instead of // the client window _TranslatePointToScreen(&topLeft); _TranslatePointToScreen(&bottomRight); const long width = base::ClampSub(bottomRight.x, topLeft.x); const long height = base::ClampSub(bottomRight.y, topLeft.y); // insert the coords coords.push_back(topLeft.x); coords.push_back(topLeft.y); coords.push_back(width); coords.push_back(height); } // Routine Description: // - moves the UTR's endpoint by moveCount times by character. // - if endpoints crossed, the degenerate range is created and both endpoints are moved // Arguments: // - moveCount - the number of times to move // - endpoint - the endpoint to move // - pAmountMoved - the number of times that the return values are "moved" // - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer // This is used for general movement, where you are not allowed to // create a degenerate range // Return Value: // - void UiaTextRangeBase::_moveEndpointByUnitCharacter(_In_ const int moveCount, _In_ const TextPatternRangeEndpoint endpoint, _Out_ gsl::not_null const pAmountMoved, _In_ const bool preventBufferEnd) { *pAmountMoved = 0; if (moveCount == 0) { return; } const bool allowBottomExclusive = !preventBufferEnd; const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; const auto& buffer = _pData->GetTextBuffer(); bool success = true; til::point target = GetEndpoint(endpoint); while (std::abs(*pAmountMoved) < std::abs(moveCount) && success) { switch (moveDirection) { case MovementDirection::Forward: success = buffer.MoveToNextGlyph(target, allowBottomExclusive); if (success) { (*pAmountMoved)++; } break; case MovementDirection::Backward: success = buffer.MoveToPreviousGlyph(target); if (success) { (*pAmountMoved)--; } break; default: break; } } SetEndpoint(endpoint, target); } // Routine Description: // - moves the UTR's endpoint by moveCount times by word. // - if endpoints crossed, the degenerate range is created and both endpoints are moved // Arguments: // - moveCount - the number of times to move // - endpoint - the endpoint to move // - pAmountMoved - the number of times that the return values are "moved" // - preventBufferEnd - when enabled, prevent endpoint from being at the end of the buffer // This is used for general movement, where you are not allowed to // create a degenerate range // Return Value: // - void UiaTextRangeBase::_moveEndpointByUnitWord(_In_ const int moveCount, _In_ const TextPatternRangeEndpoint endpoint, _Out_ gsl::not_null const pAmountMoved, _In_ const bool preventBufferEnd) { *pAmountMoved = 0; if (moveCount == 0) { return; } const bool allowBottomExclusive = !preventBufferEnd; const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; const auto& buffer = _pData->GetTextBuffer(); const auto bufferSize = _getBufferSize(); const auto bufferOrigin = bufferSize.Origin(); const auto bufferEnd = bufferSize.EndExclusive(); const auto lastCharPos = buffer.GetLastNonSpaceCharacter(bufferSize); auto resultPos = GetEndpoint(endpoint); auto nextPos = resultPos; bool success = true; while (std::abs(*pAmountMoved) < std::abs(moveCount) && success) { nextPos = resultPos; switch (moveDirection) { case MovementDirection::Forward: { if (nextPos == bufferEnd) { success = false; } else if (buffer.MoveToNextWord(nextPos, _wordDelimiters, lastCharPos)) { resultPos = nextPos; (*pAmountMoved)++; } else if (allowBottomExclusive) { resultPos = bufferEnd; (*pAmountMoved)++; } else { success = false; } break; } case MovementDirection::Backward: { if (nextPos == bufferOrigin) { success = false; } else if (buffer.MoveToPreviousWord(nextPos, _wordDelimiters)) { resultPos = nextPos; (*pAmountMoved)--; } else { resultPos = bufferOrigin; } break; } default: return; } } SetEndpoint(endpoint, resultPos); } // Routine Description: // - moves the UTR's endpoint by moveCount times by line. // - if endpoints crossed, the degenerate range is created and both endpoints are moved // - a successful movement on start entails start being at Left() // - a successful movement on end entails end being at Left() of the NEXT line // Arguments: // - moveCount - the number of times to move // - endpoint - the endpoint to move // - pAmountMoved - the number of times that the return values are "moved" // - preventBoundary - true --> the range encompasses the unit we're on; prevent movement onto boundaries // false --> act like we're just moving an endpoint; allow movement onto boundaries // Return Value: // - void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount, _In_ const TextPatternRangeEndpoint endpoint, _Out_ gsl::not_null const pAmountMoved, _In_ const bool preventBoundary) noexcept { *pAmountMoved = 0; if (moveCount == 0) { return; } const bool allowBottomExclusive = !preventBoundary; const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; const auto bufferSize = _getBufferSize(); bool success = true; auto resultPos = GetEndpoint(endpoint); while (std::abs(*pAmountMoved) < std::abs(moveCount) && success) { auto nextPos = resultPos; switch (moveDirection) { case MovementDirection::Forward: { // can't move past end if (nextPos.Y >= bufferSize.BottomInclusive()) { if (preventBoundary || nextPos == bufferSize.EndExclusive()) { success = false; break; } } nextPos.X = bufferSize.RightInclusive(); success = bufferSize.IncrementInBounds(nextPos, allowBottomExclusive); if (success) { resultPos = nextPos; (*pAmountMoved)++; } break; } case MovementDirection::Backward: { if (preventBoundary) { if (nextPos.Y == bufferSize.Top()) { // can't move past top success = false; break; } else { // GH#10924: as a non-degenerate range, we are supposed to act // like we already encompass the line. // Move to the left boundary so we try to wrap around nextPos.X = bufferSize.Left(); } } // NOTE: Automatically detects if we are trying to move past origin success = bufferSize.DecrementInBounds(nextPos, true); if (success) { nextPos.X = bufferSize.Left(); resultPos = nextPos; (*pAmountMoved)--; } break; } default: break; } } SetEndpoint(endpoint, resultPos); } // Routine Description: // - moves the UTR's endpoint by moveCount times by document. // - if endpoints crossed, the degenerate range is created and both endpoints are moved // Arguments: // - moveCount - the number of times to move // - endpoint - the endpoint to move // - pAmountMoved - the number of times that the return values are "moved" // - preventBoundary - true --> the range encompasses the unit we're on; prevent movement onto boundaries // false --> act like we're just moving an endpoint; allow movement onto boundaries // Return Value: // - void UiaTextRangeBase::_moveEndpointByUnitDocument(_In_ const int moveCount, _In_ const TextPatternRangeEndpoint endpoint, _Out_ gsl::not_null const pAmountMoved, _In_ const bool preventBoundary) noexcept { *pAmountMoved = 0; if (moveCount == 0) { return; } const MovementDirection moveDirection = (moveCount > 0) ? MovementDirection::Forward : MovementDirection::Backward; const auto bufferSize = _getBufferSize(); const auto target = GetEndpoint(endpoint); switch (moveDirection) { case MovementDirection::Forward: { const auto documentEnd = bufferSize.EndExclusive(); if (preventBoundary || target == documentEnd) { return; } else { SetEndpoint(endpoint, documentEnd); (*pAmountMoved)++; } break; } case MovementDirection::Backward: { const auto documentBegin = bufferSize.Origin(); if (preventBoundary || target == documentBegin) { return; } else { SetEndpoint(endpoint, documentBegin); (*pAmountMoved)--; } break; } default: break; } } RECT UiaTextRangeBase::_getTerminalRect() const { UiaRect result{ 0 }; IRawElementProviderFragment* pRawElementProviderFragment; THROW_IF_FAILED(_pProvider->QueryInterface(&pRawElementProviderFragment)); if (pRawElementProviderFragment) { pRawElementProviderFragment->get_BoundingRectangle(&result); } return { gsl::narrow(result.left), gsl::narrow(result.top), gsl::narrow(result.left + result.width), gsl::narrow(result.top + result.height) }; } COORD UiaTextRangeBase::_getInclusiveEnd() noexcept { auto result{ _end }; _pData->GetTextBuffer().GetSize().DecrementInBounds(result, true); return result; }