You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/LyShine/Code/Source/UiTextComponentOffsetsSelec...

230 lines
10 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "UiTextComponentOffsetsSelector.h"
#include "StringUtfUtils.h"
void UiTextComponentOffsetsSelector::ParseBatchLine(const UiTextComponent::DrawBatchLine& batchLine, float& curLineWidth)
{
// Knowing the length of the line helps with alignment calculations
lineOffsetsStack.top()->batchLineLength = batchLine.lineSize.GetX();
// The "current line index" resets to zero with each new line. This
// index allows us to index relative to the current line of text
// we're iterating on.
int curLineIndexIter = 0;
// Keep track of where m_firstIndex occurs relative to the current line.
// This is needed when m_firstIndex and m_lastIndex occur on the same line
// to obtain the selection range for that line.
int firstIndexLineIndex = 0;
// For input text, we could safely assume one DrawBatch per line,
// since we don't support marked-up input (at least for now). But
// it's easy enough to iterate through the list anyways.
for (const UiTextComponent::DrawBatch& drawBatch : batchLine.drawBatchList)
{
// Iterate character by character over DrawBatch string contents,
// looking for m_firstIndex and m_lastIndex.
Utf8::Unchecked::octet_iterator pChar(drawBatch.text.data());
while (uint32_t ch = *pChar)
{
++pChar;
if (m_indexIter == m_firstIndex)
{
firstIndexFound = true;
firstIndexLineIndex = curLineIndexIter;
// Get the width of the string of characters prior to the
// selection string. This will be used to offset the
// cursor position from the left of the start of the line.
AZStd::string unselectedPrecedingString(drawBatch.text.substr(0, firstIndexLineIndex));
lineOffsetsStack.top()->left.SetX(curLineWidth + drawBatch.font->GetTextSize(unselectedPrecedingString.c_str(), false, m_fontContext).x);
if (m_firstIndex == m_lastIndex)
{
lastIndexFound = true;
lineOffsetsStack.top()->right = AZ::Vector2::CreateZero();
break;
}
}
else if (m_indexIter == m_lastIndex)
{
lastIndexFound = true;
// The number of chars selected (selection length) for this
// line depends on whether the selection is split across multiple lines.
const int selectionLength = firstAndLastIndexOccurOnDifferentLines ? curLineIndexIter : curLineIndexIter - firstIndexLineIndex;
AZStd::string selectionString(drawBatch.text.substr(firstIndexLineIndex, selectionLength));
Vec2 rightSize = drawBatch.font->GetTextSize(selectionString.c_str(), true, m_fontContext);
lineOffsetsStack.top()->right.SetX(rightSize.x);
m_numCharsSelected += LyShine::GetUtf8StringLength(selectionString);
break;
}
// Iterate both curLineIndexIter (the index relative to this
// line) and m_indexIter (the 'global' index for iterating across
// the entire rendered string).
curLineIndexIter += LyShine::GetMultiByteCharSize(ch);
++m_indexIter;
}
// We're done iterating through the string contents of this DrawBatch
// for this line and we still haven't found m_firstIndex. In this case,
// we can add the entire width of the DrawBatch contents to the current
// line width.
if (!firstIndexFound)
{
curLineWidth += drawBatch.font->GetTextSize(drawBatch.text.c_str(), false, m_fontContext).x;
}
// If m_firstIndex has been found, but we haven't found m_lastIndex, we
// calculate curLineWidth relative to firstIndexLineIndex (the m_firstIndex
// position relative to the current line). Note that firstIndexLineIndex
// is reset to zero with each line we iterate on. This allows us to
// select the substring for the current line whether m_firstIndex occurs
// on the same line or not.
else if (!lastIndexFound)
{
int substrLength = static_cast<int>(drawBatch.text.length() - firstIndexLineIndex);
AZStd::string curSubstring(drawBatch.text.substr(firstIndexLineIndex, substrLength));
curLineWidth += drawBatch.font->GetTextSize(curSubstring.c_str(), false, m_fontContext).x;
lineOffsetsStack.top()->right.SetX(AZStd::GetMax<float>(lineOffsetsStack.top()->right.GetX(), curLineWidth));
m_numCharsSelected += LyShine::GetUtf8StringLength(curSubstring);
}
}
}
void UiTextComponentOffsetsSelector::HandleTopAndMiddleOffsets()
{
const bool topOffsetNeedsPopping = 3 == lineOffsetsStack.size();
const bool middleOffsetNeedsPopping = m_lineCounter + 1 == m_lastIndexLineNumber;
if (topOffsetNeedsPopping)
{
const float curHeightOffset = lineOffsetsStack.top()->left.GetY() + lineOffsetsStack.top()->right.GetY();
lineOffsetsStack.pop();
// We take the max here in case the top offset occurs on
// the first line (in which case the height offset would be zero).
// This either pushes the cursor to the following line
// (m_fontSize) or following lines if an offset is applied (curHeightOffset).
lineOffsetsStack.top()->left.SetY(AZStd::GetMax<float>(curHeightOffset, m_fontSize));
// Always reset right (relative) y-offset when a new left
// ("absolute") y-offset is assigned.
lineOffsetsStack.top()->right.SetY(0.0f);
}
else if (middleOffsetNeedsPopping)
{
const float curHeightOffset = lineOffsetsStack.top()->left.GetY() + lineOffsetsStack.top()->right.GetY();
lineOffsetsStack.pop();
// We need to substract m_fontSize here to "prime" for the
// fact that we'll be adding it back in, below.
lineOffsetsStack.top()->left.SetY(lineOffsetsStack.top()->left.GetY() + (curHeightOffset - m_fontSize));
// Always reset right (relative) y-offset when a new left
// ("absolute") y-offset is assigned.
lineOffsetsStack.top()->right.SetY(0.0f);
}
}
void UiTextComponentOffsetsSelector::IncrementYOffsets()
{
// We increment the left (absolute) y-offset only when we are NOT
// iterating through a "middle" section. Once we hit a middle
// section, we want to lock/freeze the left (absolute) y-offset
// position and only increment the right (relative) y-offset
// position. This allows the rendered rect to span the entirety
// of the selection.
const bool iteratingOnMiddleSection = 2 == lineOffsetsStack.size() && m_lineCounter < m_numLines;
if (!iteratingOnMiddleSection)
{
lineOffsetsStack.top()->left.SetY(lineOffsetsStack.top()->left.GetY() + m_fontSize);
// Always reset right (relative) y-offset when a new left
// ("absolute") y-offset is assigned.
lineOffsetsStack.top()->right.SetY(0.0f);
}
lineOffsetsStack.top()->right.SetY(lineOffsetsStack.top()->right.GetY() + m_fontSize);
}
void UiTextComponentOffsetsSelector::CalculateOffsets(UiTextComponent::LineOffsets& top, UiTextComponent::LineOffsets& middle, UiTextComponent::LineOffsets& bottom)
{
lineOffsetsStack.push(&bottom);
lineOffsetsStack.push(&middle);
lineOffsetsStack.push(&top);
// Iterate over each rendered line of text, operating on the top of the
// line offsets stack. The stack is popped as each section is completed.
// Since the bottom section is the last section, there's no need to pop
// it off the stack.
for (const UiTextComponent::DrawBatchLine& batchLine : m_drawBatchLines.batchLines)
{
m_lineCounter++;
float curLineWidth = 0.0f;
// X offset gets reset for every new line we iterate
lineOffsetsStack.top()->left.SetX(0.0f);
ParseBatchLine(batchLine, curLineWidth);
// Handle the special case where the index is at the end of the string
// (1 beyond the string index, technically) and there is no selection.
// For this case we want to display the cursor at the end of the string,
// so we assign the curLineWidth to the left X offset.
const bool cursorAtEndOfString = lineOffsetsStack.top()->left.GetX() == 0.0f;
if (cursorAtEndOfString && m_firstIndex == m_lastIndex)
{
lineOffsetsStack.top()->left.SetX(curLineWidth);
}
const bool noSelection = m_firstIndex == m_lastIndex;
const bool onLineHint = m_lineCounter == m_lineNumHint;
const bool onIndex = m_indexIter == m_firstIndex;
const bool shouldPlaceIndexOnThisLine = noSelection && onLineHint && onIndex;
if (shouldPlaceIndexOnThisLine)
{
firstIndexFound = lastIndexFound = true;
}
// If we still haven't found m_firstIndex, we can skip additional
// early-out, stack-popping logic, etc.
if (firstIndexFound)
{
// It's possible to have all the characters selected but never found
// m_lastIndex because m_lastIndex could be 1-beyond the string array.
// For example, if all characters are selected, then m_lastIndex is
// actually 1-beyond the array extents, so we account for that here.
const bool allCharsSelected = m_numCharsSelected > 0 && m_numCharsSelected == m_lastIndex - m_firstIndex;
if (lastIndexFound || allCharsSelected)
{
// Nothing left to do
break;
}
else
{
HandleTopAndMiddleOffsets();
firstAndLastIndexOccurOnDifferentLines = true;
}
}
// When cursor is at end of text, last and first index booleans technically
// aren't found because the cursor is one past the end of the string
// array, so execution comes to this point.
const bool cursorAtEndOfText = cursorAtEndOfString && m_lineCounter == m_numLines;
if (!cursorAtEndOfText)
{
IncrementYOffsets();
}
}
}