terminal/src/types/UiaTextRangeBase.cpp
Carlos Zamora 5deb332607
Fix UIA Word movement tests (#11253)
## Summary of the Pull Request
Fixes the 24 failing generated tests. 20 of them were fixed by enforcing the following rule: when moving backwards by word...
- a degenerate range moves to the beginning of the word, then to the word behind it.
- a non-degenerate range outright moves to the word behind it.

The fix was simple: if we're a degenerate range, check if we're at the beginning of the word. If not, move there. Otherwise, move to the word before it. See UiaTextRangeBase.cpp changes for implementation details.

Along the way, several misauthored tests were found:
- 2 generated tests:
   - Cause: MS Word considers a line break a word delimiter. We don't use line-wrapping to distinguish two separate words.
- `MovementAtExclusiveEnd` backwards word movement tests:
   - `end` will always be `writeTarget` because...
      - [degenerate range case] both `start` and `end` are moved to the beginning of the word (`writeTarget`)
      - [non-degenerate range case] from the `UiaTextRangeBase` bugfix, we should be moving to the word behind it.
   - this misauthored test was explicitly found by fixing the bug first explained here.

## References
#10925 Word navigation testing
2021-09-22 17:50:34 +00:00

1775 lines
63 KiB
C++

// 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<LONG>(point.x);
clientPoint.y = static_cast<LONG>(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<SHORT>(clientPoint.y / static_cast<LONG>(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:
// - <none>
// 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<UiaTextRangeBase*>(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<UiaTextRangeBase*>(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:
// - <none>
void UiaTextRangeBase::_expandToEnclosingUnit(TextUnit unit)
{
const auto& buffer = _pData->GetTextBuffer();
const auto bufferSize{ buffer.GetSize() };
const auto documentEnd{ _getDocumentEnd() };
// If we're past document end,
// set us to ONE BEFORE the document end.
// This allows us to expand properly.
if (bufferSize.CompareInBounds(_start, documentEnd, true) >= 0)
{
_start = documentEnd;
bufferSize.DecrementInBounds(_start, true);
}
if (unit == TextUnit_Character)
{
_start = buffer.GetGlyphStart(_start, documentEnd);
_end = buffer.GetGlyphEnd(_start, documentEnd);
}
else if (unit <= TextUnit_Word)
{
// expand to word
_start = buffer.GetWordStart(_start, _wordDelimiters, true, documentEnd);
_end = buffer.GetWordEnd(_start, _wordDelimiters, true, documentEnd);
}
else if (unit <= TextUnit_Line)
{
// expand to line
_start.X = 0;
if (_start.Y == documentEnd.y())
{
// we're on the last line
_end = documentEnd;
bufferSize.IncrementInBounds(_end, true);
}
else
{
_end.X = 0;
_end.Y = base::ClampAdd(_start.Y, 1);
}
}
else
{
// expand to document
_start = bufferSize.Origin();
_end = documentEnd;
}
}
// 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<bool> 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<UiaTextRangeBase&>(**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<UiaTextRangeBase&>(**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<UiaTextRangeBase&>(**ppRetVal), UiaTracing::AttributeType::Unsupported);
return E_NOTIMPL;
}
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
UiaTracing::TextRange::FindAttribute(*this, attributeId, val, searchBackwards, static_cast<UiaTextRangeBase&>(**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<COORD> resultFirstAnchor;
std::optional<COORD> 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<short>(std::abs(inclusiveEnd.X - _start.X + 1)) };
const auto height{ gsl::narrow_cast<short>(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<UiaTextRangeBase&>(**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<UiaTextRangeBase&>(**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 = _getOptimizedBufferSize();
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<UiaTextRangeBase&>(**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<short>(std::abs(inclusiveEnd.X - _start.X + 1)) };
const auto height{ gsl::narrow_cast<short>(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<double> 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<ULONG>(coords.size()));
if (*ppRetVal == nullptr)
{
return E_OUTOFMEMORY;
}
HRESULT hr = E_UNEXPECTED;
for (LONG i = 0; i < gsl::narrow<LONG>(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<unsigned int>{ 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<unsigned int> 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();
});
// We can abstract this movement by moving _start
// GH#7342: check if we're past the documentEnd
// If so, clamp each endpoint to the end of the document.
constexpr auto endpoint = TextPatternRangeEndpoint::TextPatternRangeEndpoint_Start;
const auto bufferSize{ _pData->GetTextBuffer().GetSize() };
const COORD documentEnd = _getDocumentEnd();
if (bufferSize.CompareInBounds(_start, documentEnd, true) > 0)
{
_start = documentEnd;
}
if (bufferSize.CompareInBounds(_end, documentEnd, true) > 0)
{
_end = documentEnd;
}
const auto wasDegenerate = IsDegenerate();
if (count != 0)
{
const auto preventBoundary = !wasDegenerate;
if (unit == TextUnit::TextUnit_Character)
{
_moveEndpointByUnitCharacter(count, endpoint, pRetVal, preventBoundary);
}
else if (unit <= TextUnit::TextUnit_Word)
{
_moveEndpointByUnitWord(count, endpoint, pRetVal, preventBoundary);
}
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();
});
// GH#7342: check if we're past the documentEnd
// If so, clamp each endpoint to the end of the document.
const auto bufferSize{ _pData->GetTextBuffer().GetSize() };
auto documentEnd = bufferSize.EndExclusive();
try
{
documentEnd = _getDocumentEnd();
}
CATCH_LOG();
if (bufferSize.CompareInBounds(_start, documentEnd, true) > 0)
{
_start = documentEnd;
}
if (bufferSize.CompareInBounds(_end, documentEnd, true) > 0)
{
_end = documentEnd;
}
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<UiaTextRangeBase*>(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<short> startScreenInfoRow = _start.Y;
const base::ClampedNumeric<short> endScreenInfoRow = _end.Y;
// screen buffer rows
const base::ClampedNumeric<short> topRow = 0;
const base::ClampedNumeric<short> 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<unsigned int>(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<short>(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<ScreenInfoUiaProviderBase*> provider = static_cast<ScreenInfoUiaProviderBase*>(_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:
// - <none>
// Return Value:
// - A viewport representing the portion of the TextBuffer that has valid text
const Viewport UiaTextRangeBase::_getOptimizedBufferSize() 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<short>(1, textBufferEnd.X);
const auto height = base::ClampAdd<short>(1, textBufferEnd.Y);
return Viewport::FromDimensions({ 0, 0 }, width, height);
}
// We consider the "document end" to be the line beneath the cursor or
// last legible character (whichever is further down). In the event where
// the last legible character is on the last line of the buffer,
// we use the "end exclusive" position (left-most point on a line one past the end of the buffer).
// NOTE: "end exclusive" is naturally computed using the heuristic above.
const til::point UiaTextRangeBase::_getDocumentEnd() const
{
const auto optimizedBufferSize{ _getOptimizedBufferSize() };
const auto& buffer{ _pData->GetTextBuffer() };
const auto lastCharPos{ buffer.GetLastNonSpaceCharacter(optimizedBufferSize) };
const auto cursorPos{ buffer.GetCursor().GetPosition() };
return { optimizedBufferSize.Left(), std::max(lastCharPos.Y, cursorPos.Y) + 1 };
}
// 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:
// - <none>
void UiaTextRangeBase::_getBoundingRect(const til::rectangle textRect, _Inout_ std::vector<double>& 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 <long,long>
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:
// - <none>
void UiaTextRangeBase::_moveEndpointByUnitCharacter(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> 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);
const auto documentEnd{ _getDocumentEnd() };
while (std::abs(*pAmountMoved) < std::abs(moveCount) && success)
{
switch (moveDirection)
{
case MovementDirection::Forward:
success = buffer.MoveToNextGlyph(target, allowBottomExclusive, documentEnd);
if (success)
{
(*pAmountMoved)++;
}
break;
case MovementDirection::Backward:
success = buffer.MoveToPreviousGlyph(target, documentEnd);
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:
// - <none>
void UiaTextRangeBase::_moveEndpointByUnitWord(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> 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 = buffer.GetSize();
const auto bufferOrigin = bufferSize.Origin();
const auto documentEnd = _getDocumentEnd();
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 (bufferSize.CompareInBounds(nextPos, documentEnd, true) >= 0)
{
success = false;
}
else if (buffer.MoveToNextWord(nextPos, _wordDelimiters, documentEnd))
{
resultPos = nextPos;
(*pAmountMoved)++;
}
else if (allowBottomExclusive)
{
resultPos = documentEnd;
(*pAmountMoved)++;
}
else
{
success = false;
}
break;
}
case MovementDirection::Backward:
{
if (nextPos == bufferOrigin)
{
success = false;
}
else if (allowBottomExclusive && _tryMoveToWordStart(buffer, documentEnd, resultPos))
{
// IMPORTANT: _tryMoveToWordStart modifies resultPos if successful
// Degenerate ranges first move to the beginning of the word,
// but if we're already at the beginning of the word, we continue
// to the next branch and move to the previous word!
(*pAmountMoved)--;
}
else if (buffer.MoveToPreviousWord(nextPos, _wordDelimiters))
{
resultPos = nextPos;
(*pAmountMoved)--;
}
else
{
resultPos = bufferOrigin;
}
break;
}
default:
return;
}
}
SetEndpoint(endpoint, resultPos);
}
// Routine Description:
// - tries to move resultingPos to the beginning of the word
// Arguments:
// - buffer - the text buffer we're operating on
// - documentEnd - the document end of the buffer (see _getDocumentEnd())
// - resultingPos - the position we're starting from and modifying
// Return Value:
// - true --> we were not at the beginning of the word, and we updated resultingPos to be so
// - false --> otherwise (we're already at the beginning of the word)
bool UiaTextRangeBase::_tryMoveToWordStart(const TextBuffer& buffer, const til::point documentEnd, COORD& resultingPos) const
{
const auto wordStart{ buffer.GetWordStart(resultingPos, _wordDelimiters, true, documentEnd) };
if (resultingPos != wordStart)
{
resultingPos = wordStart;
return true;
}
return false;
}
// 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:
// - <none>
void UiaTextRangeBase::_moveEndpointByUnitLine(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> 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 = _getOptimizedBufferSize();
auto documentEnd{ bufferSize.EndExclusive() };
try
{
documentEnd = _getDocumentEnd();
}
CATCH_LOG();
bool success = true;
auto resultPos = GetEndpoint(endpoint);
while (std::abs(*pAmountMoved) < std::abs(moveCount) && success)
{
auto nextPos = resultPos;
switch (moveDirection)
{
case MovementDirection::Forward:
{
if (nextPos.Y >= documentEnd.Y)
{
// Corner Case: we're past the limit
// Clamp us to the limit
resultPos = documentEnd;
success = false;
}
else if (preventBoundary && nextPos.Y == base::ClampSub(documentEnd.Y, 1))
{
// Corner Case: we're just before the limit
// and we're not allowed onto the exclusive end.
// Fail to move.
success = false;
}
else
{
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:
// - <none>
void UiaTextRangeBase::_moveEndpointByUnitDocument(_In_ const int moveCount,
_In_ const TextPatternRangeEndpoint endpoint,
_Out_ gsl::not_null<int*> 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 = _getOptimizedBufferSize();
const auto target = GetEndpoint(endpoint);
switch (moveDirection)
{
case MovementDirection::Forward:
{
auto documentEnd{ bufferSize.EndExclusive() };
try
{
documentEnd = _getDocumentEnd();
}
CATCH_LOG();
if (preventBoundary || bufferSize.CompareInBounds(target, documentEnd, true) >= 0)
{
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<IRawElementProviderFragment>(&pRawElementProviderFragment));
if (pRawElementProviderFragment)
{
pRawElementProviderFragment->get_BoundingRectangle(&result);
}
return {
gsl::narrow<LONG>(result.left),
gsl::narrow<LONG>(result.top),
gsl::narrow<LONG>(result.left + result.width),
gsl::narrow<LONG>(result.top + result.height)
};
}
COORD UiaTextRangeBase::_getInclusiveEnd() noexcept
{
auto result{ _end };
_pData->GetTextBuffer().GetSize().DecrementInBounds(result, true);
return result;
}