2 * Copyright (c) 2022 Samsung Electronics Co., Ltd.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
19 #include <dali-toolkit/internal/text/text-selection-handle-controller.h>
21 #include <dali/integration-api/debug.h>
25 #include <dali-toolkit/internal/text/cursor-helper-functions.h>
26 #include <dali-toolkit/internal/text/glyph-metrics-helper.h>
27 #include <dali-toolkit/internal/text/rendering/styles/character-spacing-helper-functions.h>
28 #include <dali-toolkit/internal/text/text-controller-impl-event-handler.h>
35 * @brief Struct used to calculate the selection box.
37 struct SelectionBoxInfo
45 #if defined(DEBUG_ENABLED)
46 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS");
49 const float MAX_FLOAT = std::numeric_limits<float>::max();
50 const float MIN_FLOAT = std::numeric_limits<float>::min();
51 const Dali::Toolkit::Text::CharacterDirection LTR = false; ///< Left To Right direction
61 void SelectionHandleController::Reposition(Controller::Impl& impl)
63 EventData*& eventData = impl.mEventData;
65 CharacterIndex selectionStart = eventData->mLeftSelectionPosition;
66 CharacterIndex selectionEnd = eventData->mRightSelectionPosition;
68 DecoratorPtr& decorator = eventData->mDecorator;
70 if(selectionStart == selectionEnd)
72 // Nothing to select if handles are in the same place.
73 // So, deactive Highlight box.
74 decorator->SetHighlightActive(false);
78 decorator->ClearHighlights();
80 ModelPtr& model = impl.mModel;
81 VisualModelPtr& visualModel = model->mVisualModel;
82 LogicalModelPtr& logicalModel = model->mLogicalModel;
84 const GlyphIndex* const charactersToGlyphBuffer = visualModel->mCharactersToGlyph.Begin();
85 const Length* const glyphsPerCharacterBuffer = visualModel->mGlyphsPerCharacter.Begin();
86 const GlyphInfo* const glyphsBuffer = visualModel->mGlyphs.Begin();
87 const Vector2* const positionsBuffer = visualModel->mGlyphPositions.Begin();
88 const Length* const charactersPerGlyphBuffer = visualModel->mCharactersPerGlyph.Begin();
89 const CharacterIndex* const glyphToCharacterBuffer = visualModel->mGlyphsToCharacters.Begin();
90 const CharacterDirection* const modelCharacterDirectionsBuffer = (0u != logicalModel->mCharacterDirections.Count()) ? logicalModel->mCharacterDirections.Begin() : NULL;
92 const bool isLastCharacter = selectionEnd >= logicalModel->mText.Count();
93 const CharacterDirection startDirection = ((NULL == modelCharacterDirectionsBuffer) ? false : *(modelCharacterDirectionsBuffer + selectionStart));
94 const CharacterDirection endDirection = ((NULL == modelCharacterDirectionsBuffer) ? false : *(modelCharacterDirectionsBuffer + (selectionEnd - (isLastCharacter ? 1u : 0u))));
96 // Swap the indices if the start is greater than the end.
97 const bool indicesSwapped = selectionStart > selectionEnd;
99 // Tell the decorator to flip the selection handles if needed.
100 decorator->SetSelectionHandleFlipState(indicesSwapped, startDirection, endDirection);
104 std::swap(selectionStart, selectionEnd);
107 // Get the indices to the first and last selected glyphs.
108 const CharacterIndex selectionEndMinusOne = selectionEnd - 1u;
109 const GlyphIndex glyphStart = *(charactersToGlyphBuffer + selectionStart);
110 const Length numberOfGlyphs = *(glyphsPerCharacterBuffer + selectionEndMinusOne);
111 const GlyphIndex glyphEnd = *(charactersToGlyphBuffer + selectionEndMinusOne) + ((numberOfGlyphs > 0) ? numberOfGlyphs - 1u : 0u);
112 const float modelCharacterSpacing = visualModel->GetCharacterSpacing();
113 Vector<CharacterIndex>& glyphToCharacterMap = visualModel->mGlyphsToCharacters;
114 const CharacterIndex* glyphToCharacterMapBuffer = glyphToCharacterMap.Begin();
116 // Get the character-spacing runs.
117 const Vector<CharacterSpacingGlyphRun>& characterSpacingGlyphRuns = visualModel->GetCharacterSpacingGlyphRuns();
119 // Get the lines where the glyphs are laid-out.
120 const LineRun* lineRun = visualModel->mLines.Begin();
122 LineIndex lineIndex = 0u;
123 Length numberOfLines = 0u;
124 visualModel->GetNumberOfLines(glyphStart,
125 1u + glyphEnd - glyphStart,
128 const LineIndex firstLineIndex = lineIndex;
130 // Create the structure to store some selection box info.
131 Vector<SelectionBoxInfo> selectionBoxLinesInfo;
132 selectionBoxLinesInfo.Resize(numberOfLines);
134 SelectionBoxInfo* selectionBoxInfo = selectionBoxLinesInfo.Begin();
135 selectionBoxInfo->minX = MAX_FLOAT;
136 selectionBoxInfo->maxX = MIN_FLOAT;
138 // Keep the min and max 'x' position to calculate the size and position of the highlighed text.
139 float minHighlightX = std::numeric_limits<float>::max();
140 float maxHighlightX = std::numeric_limits<float>::min();
142 Vector2 highLightPosition; // The highlight position in decorator's coords.
144 // Retrieve the first line and get the line's vertical offset, the line's height and the index to the last glyph.
146 // The line's vertical offset of all the lines before the line where the first glyph is laid-out.
147 selectionBoxInfo->lineOffset = CalculateLineOffset(visualModel->mLines,
150 // Transform to decorator's (control) coords.
151 selectionBoxInfo->lineOffset += model->mScrollPosition.y;
153 lineRun += firstLineIndex;
155 // The line height is the addition of the line ascender and the line descender.
156 // However, the line descender has a negative value, hence the subtraction also line spacing should not be included in selection height.
157 selectionBoxInfo->lineHeight = lineRun->ascender - lineRun->descender;
159 GlyphIndex lastGlyphOfLine = lineRun->glyphRun.glyphIndex + lineRun->glyphRun.numberOfGlyphs - 1u;
161 // Check if the first glyph is a ligature that must be broken like Latin ff, fi, or Arabic ﻻ, etc which needs special code.
162 const Length numberOfCharactersStart = *(charactersPerGlyphBuffer + glyphStart);
163 bool splitStartGlyph = (numberOfCharactersStart > 1u) && HasLigatureMustBreak(logicalModel->GetScript(selectionStart));
165 // Check if the last glyph is a ligature that must be broken like Latin ff, fi, or Arabic ﻻ, etc which needs special code.
166 const Length numberOfCharactersEnd = *(charactersPerGlyphBuffer + glyphEnd);
167 bool splitEndGlyph = (glyphStart != glyphEnd) && (numberOfCharactersEnd > 1u) && HasLigatureMustBreak(logicalModel->GetScript(selectionEndMinusOne));
169 // The number of quads of the selection box.
170 const unsigned int numberOfQuads = 1u + (glyphEnd - glyphStart) + ((numberOfLines > 1u) ? 2u * numberOfLines : 0u);
171 decorator->ResizeHighlightQuads(numberOfQuads);
173 // Count the actual number of quads.
174 unsigned int actualNumberOfQuads = 0u;
176 float calculatedAdvance = 0.f;
178 // Traverse the glyphs.
179 for(GlyphIndex index = glyphStart; index <= glyphEnd; ++index)
181 const float characterSpacing = GetGlyphCharacterSpacing(index, characterSpacingGlyphRuns, modelCharacterSpacing);
182 const GlyphInfo& glyph = *(glyphsBuffer + index);
183 const Vector2& position = *(positionsBuffer + index);
184 calculatedAdvance = GetCalculatedAdvance(*(logicalModel->mText.Begin() + (*(glyphToCharacterMapBuffer + index))), characterSpacing, glyph.advance);
188 // If the first glyph is a ligature that must be broken it may be needed to add only part of the glyph to the highlight box.
190 const float glyphAdvance = calculatedAdvance / static_cast<float>(numberOfCharactersStart);
191 const CharacterIndex interGlyphIndex = selectionStart - *(glyphToCharacterBuffer + glyphStart);
192 // Get the direction of the character.
193 CharacterDirection isCurrentRightToLeft = false;
194 if(nullptr != modelCharacterDirectionsBuffer) // If modelCharacterDirectionsBuffer is NULL, it means the whole text is left to right.
196 isCurrentRightToLeft = *(modelCharacterDirectionsBuffer + selectionStart);
199 // The end point could be in the middle of the ligature.
200 // Calculate the number of characters selected.
201 const Length numberOfCharacters = (glyphStart == glyphEnd) ? (selectionEnd - selectionStart) : (numberOfCharactersStart - interGlyphIndex);
203 quad.x = lineRun->alignmentOffset + position.x - glyph.xBearing + model->mScrollPosition.x + glyphAdvance * static_cast<float>(isCurrentRightToLeft ? (numberOfCharactersStart - interGlyphIndex - numberOfCharacters) : interGlyphIndex);
204 quad.y = selectionBoxInfo->lineOffset;
205 quad.z = quad.x + static_cast<float>(numberOfCharacters) * glyphAdvance;
206 quad.w = selectionBoxInfo->lineOffset + selectionBoxInfo->lineHeight;
208 // Store the min and max 'x' for each line.
209 selectionBoxInfo->minX = std::min(selectionBoxInfo->minX, quad.x);
210 selectionBoxInfo->maxX = std::max(selectionBoxInfo->maxX, quad.z);
212 decorator->AddHighlight(actualNumberOfQuads, quad);
213 ++actualNumberOfQuads;
215 splitStartGlyph = false;
219 if(splitEndGlyph && (index == glyphEnd))
221 // Equally, if the last glyph is a ligature that must be broken it may be needed to add only part of the glyph to the highlight box.
223 const float glyphAdvance = calculatedAdvance / static_cast<float>(numberOfCharactersEnd);
224 const CharacterIndex interGlyphIndex = selectionEnd - *(glyphToCharacterBuffer + glyphEnd);
225 // Get the direction of the character.
226 CharacterDirection isCurrentRightToLeft = false;
227 if(nullptr != modelCharacterDirectionsBuffer) // If modelCharacterDirectionsBuffer is NULL, it means the whole text is left to right.
229 isCurrentRightToLeft = *(modelCharacterDirectionsBuffer + selectionEnd);
232 const Length numberOfCharacters = numberOfCharactersEnd - interGlyphIndex;
234 quad.x = lineRun->alignmentOffset + position.x - glyph.xBearing + model->mScrollPosition.x + (isCurrentRightToLeft ? (glyphAdvance * static_cast<float>(numberOfCharacters)) : 0.f);
235 quad.y = selectionBoxInfo->lineOffset;
236 quad.z = quad.x + static_cast<float>(interGlyphIndex) * glyphAdvance;
237 quad.w = quad.y + selectionBoxInfo->lineHeight;
239 // Store the min and max 'x' for each line.
240 selectionBoxInfo->minX = std::min(selectionBoxInfo->minX, quad.x);
241 selectionBoxInfo->maxX = std::max(selectionBoxInfo->maxX, quad.z);
243 decorator->AddHighlight(actualNumberOfQuads,
245 ++actualNumberOfQuads;
247 splitEndGlyph = false;
251 quad.x = lineRun->alignmentOffset + position.x - glyph.xBearing + model->mScrollPosition.x;
252 quad.y = selectionBoxInfo->lineOffset;
253 quad.z = quad.x + calculatedAdvance;
254 quad.w = quad.y + selectionBoxInfo->lineHeight;
256 // Store the min and max 'x' for each line.
257 selectionBoxInfo->minX = std::min(selectionBoxInfo->minX, quad.x);
258 selectionBoxInfo->maxX = std::max(selectionBoxInfo->maxX, quad.z);
260 decorator->AddHighlight(actualNumberOfQuads,
262 ++actualNumberOfQuads;
264 // Whether to retrieve the next line.
265 if(index == lastGlyphOfLine)
268 if(lineIndex < firstLineIndex + numberOfLines)
270 float currentLineSpacing = lineRun->lineSpacing;
272 // Retrieve the next line.
275 // Get the last glyph of the new line.
276 lastGlyphOfLine = lineRun->glyphRun.glyphIndex + lineRun->glyphRun.numberOfGlyphs - 1u;
278 // Keep the offset and height of the current selection box.
279 const float currentLineOffset = selectionBoxInfo->lineOffset;
280 const float currentLineHeight = selectionBoxInfo->lineHeight;
282 // Get the selection box info for the next line.
285 selectionBoxInfo->minX = MAX_FLOAT;
286 selectionBoxInfo->maxX = MIN_FLOAT;
288 // Update the line's vertical offset.
289 selectionBoxInfo->lineOffset = currentLineOffset + currentLineHeight;
291 if(currentLineSpacing < 0)
293 selectionBoxInfo->lineOffset += currentLineSpacing;
296 // The line height is the addition of the line ascender and the line descender.
297 // However, the line descender has a negative value, hence the subtraction also line spacing should not be included in selection height.
298 selectionBoxInfo->lineHeight = lineRun->ascender - lineRun->descender;
300 if(lineRun->lineSpacing > 0)
302 selectionBoxInfo->lineHeight += lineRun->lineSpacing;
308 // Traverses all the lines and updates the min and max 'x' positions and the total height.
309 // The final width is calculated after 'boxifying' the selection.
310 for(Vector<SelectionBoxInfo>::ConstIterator it = selectionBoxLinesInfo.Begin(),
311 endIt = selectionBoxLinesInfo.End();
315 const SelectionBoxInfo& info = *it;
317 // Update the size of the highlighted text.
318 highLightSize.height += info.lineHeight;
319 minHighlightX = std::min(minHighlightX, info.minX);
320 maxHighlightX = std::max(maxHighlightX, info.maxX);
323 // Add extra geometry to 'boxify' the selection.
325 if(1u < numberOfLines)
327 // Boxify the first line.
328 lineRun = visualModel->mLines.Begin() + firstLineIndex;
329 const SelectionBoxInfo& firstSelectionBoxLineInfo = *(selectionBoxLinesInfo.Begin());
331 bool boxifyBegin = (LTR != lineRun->direction) && (LTR != startDirection);
332 bool boxifyEnd = (LTR == lineRun->direction) && (LTR == startDirection);
337 quad.y = firstSelectionBoxLineInfo.lineOffset;
338 quad.z = firstSelectionBoxLineInfo.minX;
339 quad.w = firstSelectionBoxLineInfo.lineOffset + firstSelectionBoxLineInfo.lineHeight;
341 // Boxify at the beginning of the line.
342 decorator->AddHighlight(actualNumberOfQuads,
344 ++actualNumberOfQuads;
346 // Update the size of the highlighted text.
352 quad.x = firstSelectionBoxLineInfo.maxX;
353 quad.y = firstSelectionBoxLineInfo.lineOffset;
354 quad.z = visualModel->mControlSize.width;
355 quad.w = firstSelectionBoxLineInfo.lineOffset + firstSelectionBoxLineInfo.lineHeight;
357 // Boxify at the end of the line.
358 decorator->AddHighlight(actualNumberOfQuads,
360 ++actualNumberOfQuads;
362 // Update the size of the highlighted text.
363 maxHighlightX = visualModel->mControlSize.width;
366 // Boxify the central lines.
367 if(2u < numberOfLines)
369 for(Vector<SelectionBoxInfo>::ConstIterator it = selectionBoxLinesInfo.Begin() + 1u,
370 endIt = selectionBoxLinesInfo.End() - 1u;
374 const SelectionBoxInfo& info = *it;
377 quad.y = info.lineOffset;
379 quad.w = info.lineOffset + info.lineHeight;
381 decorator->AddHighlight(actualNumberOfQuads,
383 ++actualNumberOfQuads;
386 quad.y = info.lineOffset;
387 quad.z = visualModel->mControlSize.width;
388 quad.w = info.lineOffset + info.lineHeight;
390 decorator->AddHighlight(actualNumberOfQuads,
392 ++actualNumberOfQuads;
395 // Update the size of the highlighted text.
397 maxHighlightX = visualModel->mControlSize.width;
400 // Boxify the last line.
401 lineRun = visualModel->mLines.Begin() + firstLineIndex + numberOfLines - 1u;
402 const SelectionBoxInfo& lastSelectionBoxLineInfo = *(selectionBoxLinesInfo.End() - 1u);
404 boxifyBegin = (LTR == lineRun->direction) && (LTR == endDirection);
405 boxifyEnd = (LTR != lineRun->direction) && (LTR != endDirection);
410 quad.y = lastSelectionBoxLineInfo.lineOffset;
411 quad.z = lastSelectionBoxLineInfo.minX;
412 quad.w = lastSelectionBoxLineInfo.lineOffset + lastSelectionBoxLineInfo.lineHeight;
414 // Boxify at the beginning of the line.
415 decorator->AddHighlight(actualNumberOfQuads,
417 ++actualNumberOfQuads;
419 // Update the size of the highlighted text.
425 quad.x = lastSelectionBoxLineInfo.maxX;
426 quad.y = lastSelectionBoxLineInfo.lineOffset;
427 quad.z = visualModel->mControlSize.width;
428 quad.w = lastSelectionBoxLineInfo.lineOffset + lastSelectionBoxLineInfo.lineHeight;
430 // Boxify at the end of the line.
431 decorator->AddHighlight(actualNumberOfQuads, quad);
432 ++actualNumberOfQuads;
434 // Update the size of the highlighted text.
435 maxHighlightX = visualModel->mControlSize.width;
439 // Set the actual number of quads.
440 decorator->ResizeHighlightQuads(actualNumberOfQuads);
442 // Sets the highlight's size and position. In decorator's coords.
443 // The highlight's height has been calculated above (before 'boxifying' the highlight).
444 highLightSize.width = maxHighlightX - minHighlightX;
446 highLightPosition.x = minHighlightX;
447 const SelectionBoxInfo& firstSelectionBoxLineInfo = *(selectionBoxLinesInfo.Begin());
448 highLightPosition.y = firstSelectionBoxLineInfo.lineOffset;
450 decorator->SetHighLightBox(highLightPosition, highLightSize, static_cast<float>(model->GetOutlineWidth()));
452 if(!decorator->IsSmoothHandlePanEnabled())
454 CursorInfo primaryCursorInfo;
455 impl.GetCursorPosition(eventData->mLeftSelectionPosition, primaryCursorInfo);
457 const Vector2 primaryPosition = primaryCursorInfo.primaryPosition + model->mScrollPosition;
459 decorator->SetPosition(LEFT_SELECTION_HANDLE,
461 primaryCursorInfo.lineOffset + model->mScrollPosition.y,
462 primaryCursorInfo.lineHeight);
464 CursorInfo secondaryCursorInfo;
465 impl.GetCursorPosition(eventData->mRightSelectionPosition, secondaryCursorInfo);
467 const Vector2 secondaryPosition = secondaryCursorInfo.primaryPosition + model->mScrollPosition;
469 decorator->SetPosition(RIGHT_SELECTION_HANDLE,
471 secondaryCursorInfo.lineOffset + model->mScrollPosition.y,
472 secondaryCursorInfo.lineHeight);
475 // Set the flag to update the decorator.
476 eventData->mDecoratorUpdated = true;
479 void SelectionHandleController::Reposition(Controller::Impl& impl, float visualX, float visualY, Controller::NoTextTap::Action action)
481 EventData*& eventData = impl.mEventData;
482 if(nullptr == eventData)
484 // Nothing to do if there is no text input.
488 if(impl.IsShowingPlaceholderText())
490 // Nothing to do if there is the place-holder text.
494 ModelPtr& model = impl.mModel;
495 VisualModelPtr& visualModel = model->mVisualModel;
496 const Length numberOfGlyphs = visualModel->mGlyphs.Count();
497 const Length numberOfLines = visualModel->mLines.Count();
498 if((0 == numberOfGlyphs) ||
499 (0 == numberOfLines))
501 // Nothing to do if there is no text.
505 // Find which word was selected
506 CharacterIndex selectionStart(0);
507 CharacterIndex selectionEnd(0);
508 CharacterIndex noTextHitIndex(0);
509 const bool characterHit = FindSelectionIndices(visualModel,
510 model->mLogicalModel,
517 DALI_LOG_INFO(gLogFilter, Debug::Verbose, "%p selectionStart %d selectionEnd %d\n", &impl, selectionStart, selectionEnd);
519 if(characterHit || (Controller::NoTextTap::HIGHLIGHT == action))
521 uint32_t oldStart = eventData->mLeftSelectionPosition;
522 uint32_t oldEnd = eventData->mRightSelectionPosition;
524 impl.ChangeState(EventData::SELECTING);
526 eventData->mLeftSelectionPosition = selectionStart;
527 eventData->mRightSelectionPosition = selectionEnd;
529 eventData->mUpdateLeftSelectionPosition = true;
530 eventData->mUpdateRightSelectionPosition = true;
531 eventData->mUpdateHighlightBox = true;
533 // It may happen an InputMethodContext commit event arrives before the selection event
534 // if the InputMethodContext is in pre-edit state. The commit event will set the
535 // eventData->mUpdateCursorPosition flag to true. If it's not set back
536 // to false, the highlight box won't be updated.
537 eventData->mUpdateCursorPosition = false;
539 eventData->mScrollAfterUpdatePosition = (eventData->mLeftSelectionPosition != eventData->mRightSelectionPosition);
541 // Cursor to be positioned at end of selection so if selection interrupted and edit mode restarted the cursor will be at end of selection
542 eventData->mPrimaryCursorPosition = std::max(eventData->mLeftSelectionPosition, eventData->mRightSelectionPosition);
544 if(impl.mSelectableControlInterface != nullptr)
546 impl.mSelectableControlInterface->SelectionChanged(oldStart, oldEnd, eventData->mLeftSelectionPosition, eventData->mRightSelectionPosition);
549 else if(Controller::NoTextTap::SHOW_SELECTION_POPUP == action)
551 // Nothing to select. i.e. a white space, out of bounds
552 impl.ChangeState(EventData::EDITING_WITH_POPUP);
554 eventData->mPrimaryCursorPosition = noTextHitIndex;
556 eventData->mUpdateCursorPosition = true;
557 eventData->mUpdateGrabHandlePosition = true;
558 eventData->mScrollAfterUpdatePosition = true;
559 eventData->mUpdateInputStyle = true;
561 else if(Controller::NoTextTap::NO_ACTION == action)
563 // Nothing to select. i.e. a white space, out of bounds
564 eventData->mPrimaryCursorPosition = noTextHitIndex;
566 eventData->mUpdateCursorPosition = true;
567 eventData->mUpdateGrabHandlePosition = true;
568 eventData->mScrollAfterUpdatePosition = true;
569 eventData->mUpdateInputStyle = true;
573 void SelectionHandleController::Update(Controller::Impl& impl, HandleType handleType, const CursorInfo& cursorInfo)
575 if((LEFT_SELECTION_HANDLE != handleType) &&
576 (RIGHT_SELECTION_HANDLE != handleType))
581 ModelPtr& model = impl.mModel;
582 const Vector2 cursorPosition = cursorInfo.primaryPosition + model->mScrollPosition;
584 // Sets the handle's position.
585 EventData*& eventData = impl.mEventData;
586 eventData->mDecorator->SetPosition(handleType,
588 cursorInfo.lineOffset + model->mScrollPosition.y,
589 cursorInfo.lineHeight);
591 // If selection handle at start of the text and other at end of the text then all text is selected.
592 const CharacterIndex startOfSelection = std::min(eventData->mLeftSelectionPosition, eventData->mRightSelectionPosition);
593 const CharacterIndex endOfSelection = std::max(eventData->mLeftSelectionPosition, eventData->mRightSelectionPosition);
594 eventData->mAllTextSelected = (startOfSelection == 0) && (endOfSelection == model->mLogicalModel->mText.Count());
599 } // namespace Toolkit