UiTextComponentOffsetsSelector.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "UiTextComponentOffsetsSelector.h"
  9. #include "StringUtfUtils.h"
  10. void UiTextComponentOffsetsSelector::ParseBatchLine(const UiTextComponent::DrawBatchLine& batchLine, float& curLineWidth)
  11. {
  12. // Knowing the length of the line helps with alignment calculations
  13. lineOffsetsStack.top()->batchLineLength = batchLine.lineSize.GetX();
  14. // The "current line index" resets to zero with each new line. This
  15. // index allows us to index relative to the current line of text
  16. // we're iterating on.
  17. int curLineIndexIter = 0;
  18. // Keep track of where m_firstIndex occurs relative to the current line.
  19. // This is needed when m_firstIndex and m_lastIndex occur on the same line
  20. // to obtain the selection range for that line.
  21. int firstIndexLineIndex = 0;
  22. // For input text, we could safely assume one DrawBatch per line,
  23. // since we don't support marked-up input (at least for now). But
  24. // it's easy enough to iterate through the list anyways.
  25. for (const UiTextComponent::DrawBatch& drawBatch : batchLine.drawBatchList)
  26. {
  27. // Iterate character by character over DrawBatch string contents,
  28. // looking for m_firstIndex and m_lastIndex.
  29. Utf8::Unchecked::octet_iterator pChar(drawBatch.text.data());
  30. while (uint32_t ch = *pChar)
  31. {
  32. ++pChar;
  33. if (m_indexIter == m_firstIndex)
  34. {
  35. firstIndexFound = true;
  36. firstIndexLineIndex = curLineIndexIter;
  37. // Get the width of the string of characters prior to the
  38. // selection string. This will be used to offset the
  39. // cursor position from the left of the start of the line.
  40. AZStd::string unselectedPrecedingString(drawBatch.text.substr(0, firstIndexLineIndex));
  41. lineOffsetsStack.top()->left.SetX(curLineWidth + drawBatch.font->GetTextSize(unselectedPrecedingString.c_str(), false, m_fontContext).x);
  42. if (m_firstIndex == m_lastIndex)
  43. {
  44. lastIndexFound = true;
  45. lineOffsetsStack.top()->right = AZ::Vector2::CreateZero();
  46. break;
  47. }
  48. }
  49. else if (m_indexIter == m_lastIndex)
  50. {
  51. lastIndexFound = true;
  52. // The number of chars selected (selection length) for this
  53. // line depends on whether the selection is split across multiple lines.
  54. const int selectionLength = firstAndLastIndexOccurOnDifferentLines ? curLineIndexIter : curLineIndexIter - firstIndexLineIndex;
  55. AZStd::string selectionString(drawBatch.text.substr(firstIndexLineIndex, selectionLength));
  56. Vec2 rightSize = drawBatch.font->GetTextSize(selectionString.c_str(), true, m_fontContext);
  57. lineOffsetsStack.top()->right.SetX(rightSize.x);
  58. m_numCharsSelected += LyShine::GetUtf8StringLength(selectionString);
  59. break;
  60. }
  61. // Iterate both curLineIndexIter (the index relative to this
  62. // line) and m_indexIter (the 'global' index for iterating across
  63. // the entire rendered string).
  64. curLineIndexIter += LyShine::GetMultiByteCharSize(ch);
  65. ++m_indexIter;
  66. }
  67. // We're done iterating through the string contents of this DrawBatch
  68. // for this line and we still haven't found m_firstIndex. In this case,
  69. // we can add the entire width of the DrawBatch contents to the current
  70. // line width.
  71. if (!firstIndexFound)
  72. {
  73. curLineWidth += drawBatch.font->GetTextSize(drawBatch.text.c_str(), false, m_fontContext).x;
  74. }
  75. // If m_firstIndex has been found, but we haven't found m_lastIndex, we
  76. // calculate curLineWidth relative to firstIndexLineIndex (the m_firstIndex
  77. // position relative to the current line). Note that firstIndexLineIndex
  78. // is reset to zero with each line we iterate on. This allows us to
  79. // select the substring for the current line whether m_firstIndex occurs
  80. // on the same line or not.
  81. else if (!lastIndexFound)
  82. {
  83. int substrLength = static_cast<int>(drawBatch.text.length() - firstIndexLineIndex);
  84. AZStd::string curSubstring(drawBatch.text.substr(firstIndexLineIndex, substrLength));
  85. curLineWidth += drawBatch.font->GetTextSize(curSubstring.c_str(), false, m_fontContext).x;
  86. lineOffsetsStack.top()->right.SetX(AZStd::GetMax<float>(lineOffsetsStack.top()->right.GetX(), curLineWidth));
  87. m_numCharsSelected += LyShine::GetUtf8StringLength(curSubstring);
  88. }
  89. }
  90. }
  91. void UiTextComponentOffsetsSelector::HandleTopAndMiddleOffsets()
  92. {
  93. const bool topOffsetNeedsPopping = 3 == lineOffsetsStack.size();
  94. const bool middleOffsetNeedsPopping = m_lineCounter + 1 == m_lastIndexLineNumber;
  95. if (topOffsetNeedsPopping)
  96. {
  97. const float curHeightOffset = lineOffsetsStack.top()->left.GetY() + lineOffsetsStack.top()->right.GetY();
  98. lineOffsetsStack.pop();
  99. // We take the max here in case the top offset occurs on
  100. // the first line (in which case the height offset would be zero).
  101. // This either pushes the cursor to the following line
  102. // (m_fontSize) or following lines if an offset is applied (curHeightOffset).
  103. lineOffsetsStack.top()->left.SetY(AZStd::GetMax<float>(curHeightOffset, m_fontSize));
  104. // Always reset right (relative) y-offset when a new left
  105. // ("absolute") y-offset is assigned.
  106. lineOffsetsStack.top()->right.SetY(0.0f);
  107. }
  108. else if (middleOffsetNeedsPopping)
  109. {
  110. const float curHeightOffset = lineOffsetsStack.top()->left.GetY() + lineOffsetsStack.top()->right.GetY();
  111. lineOffsetsStack.pop();
  112. // We need to substract m_fontSize here to "prime" for the
  113. // fact that we'll be adding it back in, below.
  114. lineOffsetsStack.top()->left.SetY(lineOffsetsStack.top()->left.GetY() + (curHeightOffset - m_fontSize));
  115. // Always reset right (relative) y-offset when a new left
  116. // ("absolute") y-offset is assigned.
  117. lineOffsetsStack.top()->right.SetY(0.0f);
  118. }
  119. }
  120. void UiTextComponentOffsetsSelector::IncrementYOffsets()
  121. {
  122. // We increment the left (absolute) y-offset only when we are NOT
  123. // iterating through a "middle" section. Once we hit a middle
  124. // section, we want to lock/freeze the left (absolute) y-offset
  125. // position and only increment the right (relative) y-offset
  126. // position. This allows the rendered rect to span the entirety
  127. // of the selection.
  128. const bool iteratingOnMiddleSection = 2 == lineOffsetsStack.size() && m_lineCounter < m_numLines;
  129. if (!iteratingOnMiddleSection)
  130. {
  131. lineOffsetsStack.top()->left.SetY(lineOffsetsStack.top()->left.GetY() + m_fontSize);
  132. // Always reset right (relative) y-offset when a new left
  133. // ("absolute") y-offset is assigned.
  134. lineOffsetsStack.top()->right.SetY(0.0f);
  135. }
  136. lineOffsetsStack.top()->right.SetY(lineOffsetsStack.top()->right.GetY() + m_fontSize);
  137. }
  138. void UiTextComponentOffsetsSelector::CalculateOffsets(UiTextComponent::LineOffsets& top, UiTextComponent::LineOffsets& middle, UiTextComponent::LineOffsets& bottom)
  139. {
  140. lineOffsetsStack.push(&bottom);
  141. lineOffsetsStack.push(&middle);
  142. lineOffsetsStack.push(&top);
  143. // Iterate over each rendered line of text, operating on the top of the
  144. // line offsets stack. The stack is popped as each section is completed.
  145. // Since the bottom section is the last section, there's no need to pop
  146. // it off the stack.
  147. for (const UiTextComponent::DrawBatchLine& batchLine : m_drawBatchLines.batchLines)
  148. {
  149. m_lineCounter++;
  150. float curLineWidth = 0.0f;
  151. // X offset gets reset for every new line we iterate
  152. lineOffsetsStack.top()->left.SetX(0.0f);
  153. ParseBatchLine(batchLine, curLineWidth);
  154. // Handle the special case where the index is at the end of the string
  155. // (1 beyond the string index, technically) and there is no selection.
  156. // For this case we want to display the cursor at the end of the string,
  157. // so we assign the curLineWidth to the left X offset.
  158. const bool cursorAtEndOfString = lineOffsetsStack.top()->left.GetX() == 0.0f;
  159. if (cursorAtEndOfString && m_firstIndex == m_lastIndex)
  160. {
  161. lineOffsetsStack.top()->left.SetX(curLineWidth);
  162. }
  163. const bool noSelection = m_firstIndex == m_lastIndex;
  164. const bool onLineHint = m_lineCounter == m_lineNumHint;
  165. const bool onIndex = m_indexIter == m_firstIndex;
  166. const bool shouldPlaceIndexOnThisLine = noSelection && onLineHint && onIndex;
  167. if (shouldPlaceIndexOnThisLine)
  168. {
  169. firstIndexFound = lastIndexFound = true;
  170. }
  171. // If we still haven't found m_firstIndex, we can skip additional
  172. // early-out, stack-popping logic, etc.
  173. if (firstIndexFound)
  174. {
  175. // It's possible to have all the characters selected but never found
  176. // m_lastIndex because m_lastIndex could be 1-beyond the string array.
  177. // For example, if all characters are selected, then m_lastIndex is
  178. // actually 1-beyond the array extents, so we account for that here.
  179. const bool allCharsSelected = m_numCharsSelected > 0 && m_numCharsSelected == m_lastIndex - m_firstIndex;
  180. if (lastIndexFound || allCharsSelected)
  181. {
  182. // Nothing left to do
  183. break;
  184. }
  185. else
  186. {
  187. HandleTopAndMiddleOffsets();
  188. firstAndLastIndexOccurOnDifferentLines = true;
  189. }
  190. }
  191. // When cursor is at end of text, last and first index booleans technically
  192. // aren't found because the cursor is one past the end of the string
  193. // array, so execution comes to this point.
  194. const bool cursorAtEndOfText = cursorAtEndOfString && m_lineCounter == m_numLines;
  195. if (!cursorAtEndOfText)
  196. {
  197. IncrementYOffsets();
  198. }
  199. }
  200. }