Support character-spacing tag in markup
[platform/core/uifw/dali-toolkit.git] / dali-toolkit / internal / text / cursor-helper-functions.cpp
1 /*
2  * Copyright (c) 2022 Samsung Electronics Co., Ltd.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  *
16  */
17
18 // FILE HEADER
19 #include <dali-toolkit/internal/text/cursor-helper-functions.h>
20
21 // EXTERNAL INCLUDES
22 #include <dali/integration-api/debug.h>
23
24 // INTERNAL INCLUDES
25 #include <dali-toolkit/internal/text/glyph-metrics-helper.h>
26 #include <dali-toolkit/internal/text/rendering/styles/character-spacing-helper-functions.h>
27
28 namespace
29 {
30 #if defined(DEBUG_ENABLED)
31 Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS");
32 #endif
33
34 const Dali::Toolkit::Text::CharacterDirection LTR = false; ///< Left To Right direction.
35
36 struct FindWordData
37 {
38   FindWordData(const Dali::Toolkit::Text::Character* const textBuffer,
39                Dali::Toolkit::Text::Length                 totalNumberOfCharacters,
40                Dali::Toolkit::Text::CharacterIndex         hitCharacter,
41                bool                                        isWhiteSpace,
42                bool                                        isNewParagraph)
43   : textBuffer(textBuffer),
44     totalNumberOfCharacters(totalNumberOfCharacters),
45     hitCharacter(hitCharacter),
46     foundIndex(0),
47     isWhiteSpace(isWhiteSpace),
48     isNewParagraph(isNewParagraph)
49   {
50   }
51
52   ~FindWordData()
53   {
54   }
55
56   const Dali::Toolkit::Text::Character* const textBuffer;
57   Dali::Toolkit::Text::Length                 totalNumberOfCharacters;
58   Dali::Toolkit::Text::CharacterIndex         hitCharacter;
59   Dali::Toolkit::Text::CharacterIndex         foundIndex;
60   bool                                        isWhiteSpace : 1u;
61   bool                                        isNewParagraph : 1u;
62 };
63
64 bool IsWhiteSpaceOrNewParagraph(Dali::Toolkit::Text::Character character,
65                                 bool                           isHitWhiteSpace,
66                                 bool                           isHitWhiteSpaceOrNewParagraph)
67 {
68   bool isWhiteSpaceOrNewParagraph = false;
69   if(isHitWhiteSpaceOrNewParagraph)
70   {
71     if(isHitWhiteSpace)
72     {
73       // Whether the current character is a white space. Note a new paragraph character is a white space as well but here is not wanted.
74       isWhiteSpaceOrNewParagraph = Dali::TextAbstraction::IsWhiteSpace(character) && !Dali::TextAbstraction::IsNewParagraph(character);
75     }
76     else
77     {
78       // Whether the current character is a new paragraph character.
79       isWhiteSpaceOrNewParagraph = Dali::TextAbstraction::IsNewParagraph(character);
80     }
81   }
82   else
83   {
84     // Whether the current character is a white space or a new paragraph character (note the new paragraph character is a white space as well).
85     isWhiteSpaceOrNewParagraph = Dali::TextAbstraction::IsWhiteSpace(character);
86   }
87
88   return isWhiteSpaceOrNewParagraph;
89 }
90
91 void FindStartOfWord(FindWordData& data)
92 {
93   const bool isHitWhiteSpaceOrNewParagraph = data.isWhiteSpace || data.isNewParagraph;
94
95   for(data.foundIndex = data.hitCharacter; data.foundIndex > 0; --data.foundIndex)
96   {
97     const Dali::Toolkit::Text::Character character = *(data.textBuffer + data.foundIndex - 1u);
98
99     const bool isWhiteSpaceOrNewParagraph = IsWhiteSpaceOrNewParagraph(character,
100                                                                        data.isWhiteSpace,
101                                                                        isHitWhiteSpaceOrNewParagraph);
102
103     if(isHitWhiteSpaceOrNewParagraph != isWhiteSpaceOrNewParagraph)
104     {
105       break;
106     }
107   }
108 }
109
110 void FindEndOfWord(FindWordData& data)
111 {
112   const bool isHitWhiteSpaceOrNewParagraph = data.isWhiteSpace || data.isNewParagraph;
113
114   for(data.foundIndex = data.hitCharacter + 1u; data.foundIndex < data.totalNumberOfCharacters; ++data.foundIndex)
115   {
116     const Dali::Toolkit::Text::Character character = *(data.textBuffer + data.foundIndex);
117
118     const bool isWhiteSpaceOrNewParagraph = IsWhiteSpaceOrNewParagraph(character,
119                                                                        data.isWhiteSpace,
120                                                                        isHitWhiteSpaceOrNewParagraph);
121
122     if(isHitWhiteSpaceOrNewParagraph != isWhiteSpaceOrNewParagraph)
123     {
124       break;
125     }
126   }
127 }
128
129 } //namespace
130
131 namespace Dali
132 {
133 namespace Toolkit
134 {
135 namespace Text
136 {
137 LineIndex GetClosestLine(VisualModelPtr visualModel,
138                          float          visualY,
139                          bool&          matchedLine)
140 {
141   float     totalHeight = 0.f;
142   LineIndex lineIndex   = 0;
143   matchedLine           = false;
144
145   if(visualY < 0.f)
146   {
147     return 0;
148   }
149
150   const Vector<LineRun>& lines = visualModel->mLines;
151
152   for(Vector<LineRun>::ConstIterator it    = lines.Begin(),
153                                      endIt = lines.End();
154       it != endIt;
155       ++it, ++lineIndex)
156   {
157     const LineRun& lineRun    = *it;
158     bool           isLastLine = (it + 1 == endIt);
159
160     totalHeight += GetLineHeight(lineRun, isLastLine);
161
162     if(visualY < totalHeight)
163     {
164       matchedLine = true;
165       return lineIndex;
166     }
167   }
168
169   if(lineIndex == 0)
170   {
171     return 0;
172   }
173
174   return lineIndex - 1u;
175 }
176
177 float CalculateLineOffset(const Vector<LineRun>& lines,
178                           LineIndex              lineIndex)
179 {
180   float offset = 0.f;
181
182   for(Vector<LineRun>::ConstIterator it    = lines.Begin(),
183                                      endIt = lines.Begin() + lineIndex;
184       it != endIt;
185       ++it)
186   {
187     const LineRun& lineRun    = *it;
188     bool           isLastLine = (it + 1 == lines.End());
189
190     offset += GetLineHeight(lineRun, isLastLine);
191   }
192
193   return offset;
194 }
195
196 CharacterIndex GetClosestCursorIndex(VisualModelPtr         visualModel,
197                                      LogicalModelPtr        logicalModel,
198                                      MetricsPtr             metrics,
199                                      float                  visualX,
200                                      float                  visualY,
201                                      CharacterHitTest::Mode mode,
202                                      bool&                  matchedCharacter)
203 {
204   DALI_LOG_INFO(gLogFilter, Debug::Verbose, "GetClosestCursorIndex, closest visualX %f visualY %f\n", visualX, visualY);
205
206   // Whether there is a hit on a glyph.
207   matchedCharacter = false;
208
209   CharacterIndex logicalIndex = 0;
210
211   const Length totalNumberOfGlyphs = visualModel->mGlyphs.Count();
212   const Length totalNumberOfLines  = visualModel->mLines.Count();
213   if((0 == totalNumberOfGlyphs) ||
214      (0 == totalNumberOfLines))
215   {
216     return logicalIndex;
217   }
218
219   // Get the character-spacing runs.
220   const Vector<CharacterSpacingGlyphRun>& characterSpacingGlyphRuns = visualModel->GetCharacterSpacingGlyphRuns();
221   const float                             modelCharacterSpacing     = visualModel->GetCharacterSpacing();
222
223   // Whether there is a hit on a line.
224   bool matchedLine = false;
225
226   // Find which line is closest.
227   const LineIndex lineIndex = Text::GetClosestLine(visualModel,
228                                                    visualY,
229                                                    matchedLine);
230
231   if(!matchedLine && (CharacterHitTest::TAP == mode))
232   {
233     // Return the first or the last character if the touch point doesn't hit a line.
234     return (visualY < 0.f) ? 0 : logicalModel->mText.Count();
235   }
236
237   // Convert from text's coords to line's coords.
238   const LineRun& line = *(visualModel->mLines.Begin() + lineIndex);
239
240   // Transform the tap point from text's coords to line's coords.
241   visualX -= line.alignmentOffset;
242
243   // Get the positions of the glyphs.
244   const Vector2* const positionsBuffer = visualModel->mGlyphPositions.Begin();
245
246   // Get the character to glyph conversion table.
247   const GlyphIndex* const charactersToGlyphBuffer = visualModel->mCharactersToGlyph.Begin();
248
249   // Get the glyphs per character table.
250   const Length* const glyphsPerCharacterBuffer = visualModel->mGlyphsPerCharacter.Begin();
251
252   // Get the characters per glyph table.
253   const Length* const charactersPerGlyphBuffer = visualModel->mCharactersPerGlyph.Begin();
254
255   // Get the glyph's info buffer.
256   const GlyphInfo* const glyphInfoBuffer = visualModel->mGlyphs.Begin();
257
258   const CharacterIndex startCharacter = line.characterRun.characterIndex;
259   const CharacterIndex endCharacter   = line.characterRun.characterIndex + line.characterRun.numberOfCharacters;
260   DALI_ASSERT_DEBUG(endCharacter <= logicalModel->mText.Count() && "Invalid line info");
261
262   // Whether this line is a bidirectional line.
263   const bool bidiLineFetched = logicalModel->FetchBidirectionalLineInfo(startCharacter);
264
265   // The character's direction buffer.
266   const CharacterDirection* const directionsBuffer = bidiLineFetched ? logicalModel->mCharacterDirections.Begin() : NULL;
267
268   // Whether the touch point if before the first glyph.
269   bool isBeforeFirstGlyph = false;
270
271   // Traverses glyphs in visual order. To do that use the visual to logical conversion table.
272   CharacterIndex          visualIndex               = startCharacter;
273   Length                  numberOfVisualCharacters  = 0;
274   float                   calculatedAdvance         = 0.f;
275   Vector<CharacterIndex>& glyphToCharacterMap       = visualModel->mGlyphsToCharacters;
276   const CharacterIndex*   glyphToCharacterMapBuffer = glyphToCharacterMap.Begin();
277   for(; visualIndex < endCharacter; ++visualIndex)
278   {
279     // The character in logical order.
280     const CharacterIndex     characterLogicalOrderIndex = (bidiLineFetched ? logicalModel->GetLogicalCharacterIndex(visualIndex) : visualIndex);
281     const CharacterDirection direction                  = (bidiLineFetched ? *(directionsBuffer + characterLogicalOrderIndex) : LTR);
282
283     // The number of glyphs for that character
284     const Length numberOfGlyphs = *(glyphsPerCharacterBuffer + characterLogicalOrderIndex);
285     ++numberOfVisualCharacters;
286
287     if(0 != numberOfGlyphs)
288     {
289       // Get the first character/glyph of the group of glyphs.
290       const CharacterIndex firstVisualCharacterIndex  = 1u + visualIndex - numberOfVisualCharacters;
291       const CharacterIndex firstLogicalCharacterIndex = (bidiLineFetched ? logicalModel->GetLogicalCharacterIndex(firstVisualCharacterIndex) : firstVisualCharacterIndex);
292       const GlyphIndex     firstLogicalGlyphIndex     = *(charactersToGlyphBuffer + firstLogicalCharacterIndex);
293
294       // Get the metrics for the group of glyphs.
295       GlyphMetrics glyphMetrics;
296       const float  characterSpacing = GetGlyphCharacterSpacing(firstLogicalGlyphIndex, characterSpacingGlyphRuns, modelCharacterSpacing);
297       calculatedAdvance             = GetCalculatedAdvance(*(logicalModel->mText.Begin() + (*(glyphToCharacterMapBuffer + firstLogicalGlyphIndex))), characterSpacing, (*(visualModel->mGlyphs.Begin() + firstLogicalGlyphIndex)).advance);
298       GetGlyphsMetrics(firstLogicalGlyphIndex,
299                        numberOfGlyphs,
300                        glyphMetrics,
301                        glyphInfoBuffer,
302                        metrics,
303                        calculatedAdvance);
304
305       // Get the position of the first glyph.
306       const Vector2& position = *(positionsBuffer + firstLogicalGlyphIndex);
307
308       if(startCharacter == visualIndex)
309       {
310         const float glyphPosition = -glyphMetrics.xBearing + position.x;
311
312         if(visualX < glyphPosition)
313         {
314           isBeforeFirstGlyph = true;
315           break;
316         }
317       }
318
319       // Whether the glyph can be split, like Latin ligatures fi, ff or Arabic (ل + ا).
320       Length numberOfCharacters = *(charactersPerGlyphBuffer + firstLogicalGlyphIndex);
321       if(direction != LTR)
322       {
323         // As characters are being traversed in visual order,
324         // for right to left ligatures, the character which contains the
325         // number of glyphs in the table is found first.
326         // Jump the number of characters to the next glyph is needed.
327
328         if(0 == numberOfCharacters)
329         {
330           // TODO: This is a workaround to fix an issue with complex characters in the arabic
331           // script like i.e. رّ or الأَبْجَدِيَّة العَرَبِيَّة
332           // There are characters that are not shaped in one glyph but in combination with
333           // the next one generates two of them.
334           // The visual to logical conversion table have characters in different order than
335           // expected even if all of them are arabic.
336
337           // The workaround doesn't fix the issue completely but it prevents the application
338           // to hang in an infinite loop.
339
340           // Find the number of characters.
341           for(GlyphIndex index = firstLogicalGlyphIndex + 1u;
342               (0 == numberOfCharacters) && (index < totalNumberOfGlyphs);
343               ++index)
344           {
345             numberOfCharacters = *(charactersPerGlyphBuffer + index);
346           }
347
348           if(2u > numberOfCharacters)
349           {
350             continue;
351           }
352
353           --numberOfCharacters;
354         }
355
356         visualIndex += numberOfCharacters - 1u;
357       }
358
359       // Get the script of the character.
360       const Script script = logicalModel->GetScript(characterLogicalOrderIndex);
361
362       const bool   isInterglyphIndex = (numberOfCharacters > numberOfGlyphs) && HasLigatureMustBreak(script);
363       const Length numberOfBlocks    = isInterglyphIndex ? numberOfCharacters : 1u;
364       const float  glyphAdvance      = glyphMetrics.advance / static_cast<float>(numberOfBlocks);
365
366       CharacterIndex index = 0;
367       for(; index < numberOfBlocks; ++index)
368       {
369         // Find the mid-point of the area containing the glyph
370         const float glyphCenter = -glyphMetrics.xBearing + position.x + (static_cast<float>(index) + 0.5f) * glyphAdvance;
371
372         if(visualX < glyphCenter)
373         {
374           matchedCharacter = true;
375           break;
376         }
377       }
378
379       if(matchedCharacter)
380       {
381         // If the glyph is shaped from more than one character, it matches the character of the glyph.
382         visualIndex = firstVisualCharacterIndex + index;
383         break;
384       }
385
386       numberOfVisualCharacters = 0;
387     }
388   } // for characters in visual order.
389
390   // The number of characters of the whole text.
391   const Length totalNumberOfCharacters = logicalModel->mText.Count();
392
393   // Return the logical position of the cursor in characters.
394
395   if(!matchedCharacter)
396   {
397     if(isBeforeFirstGlyph)
398     {
399       // If no character is matched, then the first character (in visual order) of the line is used.
400       visualIndex = startCharacter;
401     }
402     else
403     {
404       // If no character is matched, then the last character (in visual order) of the line is used.
405       visualIndex = endCharacter;
406     }
407   }
408
409   // Get the paragraph direction.
410   const CharacterDirection paragraphDirection = line.direction;
411
412   if(totalNumberOfCharacters != visualIndex)
413   {
414     // The visual index is not at the end of the text.
415
416     if(LTR == paragraphDirection)
417     {
418       // The paragraph direction is left to right.
419
420       if(visualIndex == endCharacter)
421       {
422         // It places the cursor just before the last character in visual order.
423         // i.e. it places the cursor just before the '\n' or before the last character
424         // if there is a long line with no word breaks which is wrapped.
425
426         // It doesn't check if the closest line is the last one like the RTL branch below
427         // because the total number of characters is different than the visual index and
428         // the visual index is the last character of the line.
429         --visualIndex;
430       }
431     }
432     else
433     {
434       // The paragraph direction is right to left.
435
436       if((lineIndex != totalNumberOfLines - 1u) && // is not the last line.
437          (visualIndex == startCharacter))
438       {
439         // It places the cursor just after the first character in visual order.
440         // i.e. it places the cursor just after the '\n' or after the last character
441         // if there is a long line with no word breaks which is wrapped.
442
443         // If the last line doesn't end with '\n' it won't increase the visual index
444         // placing the cursor at the beginning of the line (in visual order).
445         ++visualIndex;
446       }
447     }
448   }
449   else
450   {
451     // The visual index is at the end of text.
452
453     // If the text ends with a new paragraph character i.e. a '\n', an extra line with no characters is added at the end of the text.
454     // This branch checks if the closest line is the one with the last '\n'. If it is, it decrements the visual index to place
455     // the cursor just before the last '\n'.
456
457     if((lineIndex != totalNumberOfLines - 1u) &&
458        TextAbstraction::IsNewParagraph(*(logicalModel->mText.Begin() + visualIndex - 1u)))
459     {
460       --visualIndex;
461     }
462   }
463
464   logicalIndex = (bidiLineFetched ? logicalModel->GetLogicalCursorIndex(visualIndex) : visualIndex);
465
466   DALI_LOG_INFO(gLogFilter, Debug::Verbose, "closest visualIndex %d logicalIndex %d\n", visualIndex, logicalIndex);
467
468   DALI_ASSERT_DEBUG((logicalIndex <= logicalModel->mText.Count() && logicalIndex >= 0) && "GetClosestCursorIndex - Out of bounds index");
469
470   return logicalIndex;
471 }
472
473 void GetCursorPosition(GetCursorPositionParameters& parameters,
474                        float                        defaultFontLineHeight,
475                        CursorInfo&                  cursorInfo)
476 {
477   const LineRun* const modelLines = parameters.visualModel->mLines.Begin();
478   if(NULL == modelLines)
479   {
480     // Nothing to do.
481     return;
482   }
483
484   // Whether the logical cursor position is at the end of the whole text.
485   const bool isLastPosition = parameters.logicalModel->mText.Count() == parameters.logical;
486
487   // Get the line where the character is laid-out.
488   const CharacterIndex characterOfLine = isLastPosition ? (parameters.logical - 1u) : parameters.logical;
489
490   // Whether the cursor is in the last position and the last position is a new paragraph character.
491   const bool isLastNewParagraph = parameters.isMultiline && isLastPosition && TextAbstraction::IsNewParagraph(*(parameters.logicalModel->mText.Begin() + characterOfLine));
492
493   const LineIndex lineIndex = parameters.visualModel->GetLineOfCharacter(characterOfLine);
494   const LineRun&  line      = *(modelLines + lineIndex);
495
496   CharacterIndex index;
497   GlyphMetrics   glyphMetrics;
498   MetricsPtr&    metrics        = parameters.metrics;
499   GlyphIndex     glyphIndex     = 0u;
500   Length         numberOfGlyphs = 0u;
501
502   if(isLastNewParagraph)
503   {
504     // The cursor is in a new line with no characters. Place the cursor in that line.
505     const LineIndex newLineIndex = lineIndex + 1u;
506     const LineRun&  newLine      = *(modelLines + newLineIndex);
507
508     cursorInfo.isSecondaryCursor = false;
509
510     // Set the line offset and height.
511     cursorInfo.lineOffset = CalculateLineOffset(parameters.visualModel->mLines,
512                                                 newLineIndex);
513
514     // The line height is the addition of the line ascender and the line descender.
515     // However, the line descender has a negative value, hence the subtraction also line spacing should not be included in cursor height.
516     cursorInfo.lineHeight = newLine.ascender - newLine.descender;
517
518     index                                = 0u;
519     const Length totalNumberOfCharacters = parameters.logicalModel->mText.Count();
520     if(totalNumberOfCharacters > 0u)
521     {
522       index = totalNumberOfCharacters - 1u;
523     }
524
525     GetGlyphMetricsFromCharacterIndex(index, parameters.visualModel, parameters.logicalModel, metrics, glyphMetrics, glyphIndex, numberOfGlyphs);
526
527     // Set the primary cursor's height.
528     // The primary cursor height will take the font height of the last character and if there are no characters, it'll take the default font line height.
529     cursorInfo.primaryCursorHeight = (totalNumberOfCharacters > 0) ? (cursorInfo.isSecondaryCursor ? 0.5f * glyphMetrics.fontHeight : glyphMetrics.fontHeight) : defaultFontLineHeight;
530
531     // Set the primary cursor's position.
532     cursorInfo.primaryPosition.x = (LTR == line.direction) ? newLine.alignmentOffset : parameters.visualModel->mControlSize.width - newLine.alignmentOffset;
533     cursorInfo.primaryPosition.y = cursorInfo.lineOffset;
534   }
535   else
536   {
537     // Whether this line is a bidirectional line.
538     const bool bidiLineFetched = parameters.logicalModel->FetchBidirectionalLineInfo(characterOfLine);
539
540     // Check if the logical position is the first or the last one of the line.
541     const bool isFirstPositionOfLine = line.characterRun.characterIndex == parameters.logical;
542     const bool isLastPositionOfLine  = line.characterRun.characterIndex + line.characterRun.numberOfCharacters == parameters.logical;
543
544     // 'logical' is the logical 'cursor' index.
545     // Get the next and current logical 'character' index.
546     const CharacterIndex characterIndex     = isFirstPositionOfLine ? parameters.logical : parameters.logical - 1u;
547     const CharacterIndex nextCharacterIndex = isLastPositionOfLine ? characterIndex : parameters.logical;
548
549     // The character's direction buffer.
550     const CharacterDirection* const directionsBuffer = bidiLineFetched ? parameters.logicalModel->mCharacterDirections.Begin() : NULL;
551
552     CharacterDirection isCurrentRightToLeft = false;
553     CharacterDirection isNextRightToLeft    = false;
554     if(bidiLineFetched) // If bidiLineFetched is false, it means the whole text is left to right.
555     {
556       isCurrentRightToLeft = *(directionsBuffer + characterIndex);
557       isNextRightToLeft    = *(directionsBuffer + nextCharacterIndex);
558     }
559
560     // Get the paragraph's direction.
561     const CharacterDirection isRightToLeftParagraph = line.direction;
562
563     // Check whether there is an alternative position:
564     cursorInfo.isSecondaryCursor = ((!isLastPositionOfLine && (isCurrentRightToLeft != isNextRightToLeft)) ||
565                                     (isLastPositionOfLine && (isRightToLeftParagraph != isCurrentRightToLeft)) ||
566                                     (isFirstPositionOfLine && (isRightToLeftParagraph != isCurrentRightToLeft)));
567
568     // Set the line offset and height.
569     cursorInfo.lineOffset = CalculateLineOffset(parameters.visualModel->mLines,
570                                                 lineIndex);
571
572     // The line height is the addition of the line ascender and the line descender.
573     // However, the line descender has a negative value, hence the subtraction also line spacing should not be included in cursor height.
574     cursorInfo.lineHeight = line.ascender - line.descender;
575
576     // Calculate the primary cursor.
577
578     index = characterIndex;
579     if(cursorInfo.isSecondaryCursor)
580     {
581       // If there is a secondary position, the primary cursor may be in a different place than the logical index.
582
583       if(isLastPositionOfLine)
584       {
585         // The position of the cursor after the last character needs special
586         // care depending on its direction and the direction of the paragraph.
587
588         // Need to find the first character after the last character with the paragraph's direction.
589         // i.e l0 l1 l2 r0 r1 should find r0.
590
591         index = isRightToLeftParagraph ? line.characterRun.characterIndex : line.characterRun.characterIndex + line.characterRun.numberOfCharacters - 1u;
592         if(bidiLineFetched)
593         {
594           index = parameters.logicalModel->GetLogicalCharacterIndex(index);
595         }
596       }
597       else if(isFirstPositionOfLine)
598       {
599         index = isRightToLeftParagraph ? line.characterRun.characterIndex + line.characterRun.numberOfCharacters - 1u : line.characterRun.characterIndex;
600         if(bidiLineFetched)
601         {
602           index = parameters.logicalModel->GetLogicalCharacterIndex(index);
603         }
604       }
605       else
606       {
607         index = (isRightToLeftParagraph == isCurrentRightToLeft) ? characterIndex : nextCharacterIndex;
608       }
609     }
610
611     const Length* const         charactersPerGlyphBuffer = parameters.visualModel->mCharactersPerGlyph.Begin();
612     const CharacterIndex* const glyphsToCharactersBuffer = parameters.visualModel->mGlyphsToCharacters.Begin();
613     const Vector2* const        glyphPositionsBuffer     = parameters.visualModel->mGlyphPositions.Begin();
614     const float                 modelCharacterSpacing    = parameters.visualModel->GetCharacterSpacing();
615
616     const Vector<CharacterSpacingGlyphRun>& characterSpacingGlyphRuns = parameters.visualModel->GetCharacterSpacingGlyphRuns();
617
618     // Get the metrics for the group of glyphs.
619     GetGlyphMetricsFromCharacterIndex(index, parameters.visualModel, parameters.logicalModel, metrics, glyphMetrics, glyphIndex, numberOfGlyphs);
620
621     // Convert the cursor position into the glyph position.
622     const GlyphIndex primaryGlyphIndex         = glyphIndex;
623     const Length     primaryNumberOfCharacters = *(charactersPerGlyphBuffer + primaryGlyphIndex);
624
625     // Whether to add the glyph's advance to the cursor position.
626     // i.e if the paragraph is left to right and the logical cursor is zero, the position is the position of the first glyph and the advance is not added,
627     //     if the logical cursor is one, the position is the position of the first glyph and the advance is added.
628     // A 'truth table' was build and an online Karnaugh map tool was used to simplify the logic.
629     //
630     // FLCP A
631     // ------
632     // 0000 1
633     // 0001 1
634     // 0010 0
635     // 0011 0
636     // 0100 1
637     // 0101 0
638     // 0110 1
639     // 0111 0
640     // 1000 0
641     // 1001 1
642     // 1010 0
643     // 1011 1
644     // 1100 x
645     // 1101 x
646     // 1110 x
647     // 1111 x
648     //
649     // Where F -> isFirstPosition
650     //       L -> isLastPosition
651     //       C -> isCurrentRightToLeft
652     //       P -> isRightToLeftParagraph
653     //       A -> Whether to add the glyph's advance.
654
655     const bool addGlyphAdvance = ((isLastPositionOfLine && !isRightToLeftParagraph) ||
656                                   (isFirstPositionOfLine && isRightToLeftParagraph) ||
657                                   (!isFirstPositionOfLine && !isLastPosition && !isCurrentRightToLeft));
658
659     float glyphAdvance = addGlyphAdvance ? (glyphMetrics.advance) : 0.f;
660
661     if(!isLastPositionOfLine &&
662        (primaryNumberOfCharacters > 1u))
663     {
664       const CharacterIndex firstIndex = *(glyphsToCharactersBuffer + primaryGlyphIndex);
665
666       bool isCurrentRightToLeft = false;
667       if(bidiLineFetched) // If bidiLineFetched is false, it means the whole text is left to right.
668       {
669         isCurrentRightToLeft = *(directionsBuffer + index);
670       }
671
672       Length numberOfGlyphAdvance = (isFirstPositionOfLine ? 0 : 1u) + characterIndex - firstIndex;
673       if(isCurrentRightToLeft)
674       {
675         numberOfGlyphAdvance = primaryNumberOfCharacters - numberOfGlyphAdvance;
676       }
677
678       glyphAdvance = static_cast<float>(numberOfGlyphAdvance) * (glyphMetrics.advance) / static_cast<float>(primaryNumberOfCharacters);
679     }
680
681     // Get the glyph position and x bearing (in the line's coords).
682     const Vector2& primaryPosition = *(glyphPositionsBuffer + primaryGlyphIndex);
683
684     // Set the primary cursor's height.
685     cursorInfo.primaryCursorHeight = cursorInfo.isSecondaryCursor ? 0.5f * glyphMetrics.fontHeight : glyphMetrics.fontHeight;
686
687     cursorInfo.glyphOffset = line.ascender - glyphMetrics.ascender;
688     // Set the primary cursor's position.
689     cursorInfo.primaryPosition.x = -glyphMetrics.xBearing + primaryPosition.x + glyphAdvance;
690     cursorInfo.primaryPosition.y = cursorInfo.lineOffset + cursorInfo.glyphOffset;
691
692     // Transform the cursor info from line's coords to text's coords.
693     cursorInfo.primaryPosition.x += line.alignmentOffset;
694
695     // Calculate the secondary cursor.
696     if(cursorInfo.isSecondaryCursor)
697     {
698       // Set the secondary cursor's height.
699       cursorInfo.secondaryCursorHeight = 0.5f * glyphMetrics.fontHeight;
700
701       CharacterIndex index = characterIndex;
702       if(!isLastPositionOfLine)
703       {
704         index = (isRightToLeftParagraph == isCurrentRightToLeft) ? nextCharacterIndex : characterIndex;
705       }
706
707       GetGlyphMetricsFromCharacterIndex(index, parameters.visualModel, parameters.logicalModel, metrics, glyphMetrics, glyphIndex, numberOfGlyphs);
708
709       const GlyphIndex secondaryGlyphIndex = glyphIndex;
710       const Vector2&   secondaryPosition   = *(glyphPositionsBuffer + secondaryGlyphIndex);
711
712       // Set the secondary cursor's position.
713
714       // FCP A
715       // ------
716       // 000 1
717       // 001 x
718       // 010 0
719       // 011 0
720       // 100 x
721       // 101 0
722       // 110 1
723       // 111 x
724       //
725       // Where F -> isFirstPosition
726       //       C -> isCurrentRightToLeft
727       //       P -> isRightToLeftParagraph
728       //       A -> Whether to add the glyph's advance.
729
730       const bool addGlyphAdvance = ((!isFirstPositionOfLine && !isCurrentRightToLeft) ||
731                                     (isFirstPositionOfLine && !isRightToLeftParagraph));
732
733       const float characterSpacing   = GetGlyphCharacterSpacing(secondaryGlyphIndex, characterSpacingGlyphRuns, modelCharacterSpacing);
734       cursorInfo.secondaryPosition.x = -glyphMetrics.xBearing + secondaryPosition.x + (addGlyphAdvance ? (glyphMetrics.advance + characterSpacing) : 0.f);
735       cursorInfo.secondaryPosition.y = cursorInfo.lineOffset + cursorInfo.lineHeight - cursorInfo.secondaryCursorHeight;
736
737       // Transform the cursor info from line's coords to text's coords.
738       cursorInfo.secondaryPosition.x += line.alignmentOffset;
739     }
740   }
741 }
742
743 bool FindSelectionIndices(VisualModelPtr  visualModel,
744                           LogicalModelPtr logicalModel,
745                           MetricsPtr      metrics,
746                           float           visualX,
747                           float           visualY,
748                           CharacterIndex& startIndex,
749                           CharacterIndex& endIndex,
750                           CharacterIndex& noTextHitIndex)
751 {
752   /*
753   Hit character                                           Select
754 |-------------------------------------------------------|------------------------------------------|
755 | On a word                                             | The word                                 |
756 | On a single white space between words                 | The word before or after the white space |
757 | On one of the multiple contiguous white spaces        | The white spaces                         |
758 | On a single white space which is in the position zero | The white space and the next word        |
759 | On a new paragraph character                          | The word or group of white spaces before |
760 |-------------------------------------------------------|------------------------------------------|
761 */
762   const Length totalNumberOfCharacters = logicalModel->mText.Count();
763   startIndex                           = 0;
764   endIndex                             = 0;
765   noTextHitIndex                       = 0;
766
767   if(0 == totalNumberOfCharacters)
768   {
769     // Nothing to do if the model is empty.
770     return false;
771   }
772
773   bool           matchedCharacter = false;
774   CharacterIndex hitCharacter     = Text::GetClosestCursorIndex(visualModel,
775                                                             logicalModel,
776                                                             metrics,
777                                                             visualX,
778                                                             visualY,
779                                                             CharacterHitTest::TAP,
780                                                             matchedCharacter);
781
782   if(!matchedCharacter)
783   {
784     noTextHitIndex = hitCharacter;
785   }
786
787   DALI_ASSERT_DEBUG((hitCharacter <= totalNumberOfCharacters) && "GetClosestCursorIndex returned out of bounds index");
788
789   if(hitCharacter >= totalNumberOfCharacters)
790   {
791     // Closest hit character is the last character.
792     if(hitCharacter == totalNumberOfCharacters)
793     {
794       hitCharacter--; //Hit character index set to last character in logical model
795     }
796     else
797     {
798       // hitCharacter is out of bounds
799       return false;
800     }
801   }
802
803   const Character* const textBuffer = logicalModel->mText.Begin();
804
805   startIndex = hitCharacter;
806   endIndex   = hitCharacter;
807
808   // Whether the hit character is a new paragraph character.
809   const bool isHitCharacterNewParagraph = TextAbstraction::IsNewParagraph(*(textBuffer + hitCharacter));
810
811   // Whether the hit character is a white space. Note a new paragraph character is a white space as well but here is not wanted.
812   const bool isHitCharacterWhiteSpace = TextAbstraction::IsWhiteSpace(*(textBuffer + hitCharacter)) && !isHitCharacterNewParagraph;
813
814   FindWordData data(textBuffer,
815                     totalNumberOfCharacters,
816                     hitCharacter,
817                     isHitCharacterWhiteSpace,
818                     isHitCharacterNewParagraph);
819
820   if(isHitCharacterNewParagraph)
821   {
822     // Find the first character before the hit one which is not a new paragraph character.
823
824     if(hitCharacter > 0)
825     {
826       endIndex = hitCharacter - 1u;
827       for(; endIndex > 0; --endIndex)
828       {
829         const Dali::Toolkit::Text::Character character = *(data.textBuffer + endIndex);
830
831         if(!Dali::TextAbstraction::IsNewParagraph(character))
832         {
833           break;
834         }
835       }
836     }
837
838     data.hitCharacter   = endIndex;
839     data.isNewParagraph = false;
840     data.isWhiteSpace   = TextAbstraction::IsWhiteSpace(*(textBuffer + data.hitCharacter));
841   }
842
843   // Find the start of the word.
844   FindStartOfWord(data);
845   startIndex = data.foundIndex;
846
847   // Find the end of the word.
848   FindEndOfWord(data);
849   endIndex = data.foundIndex;
850
851   if(1u == (endIndex - startIndex))
852   {
853     if(isHitCharacterWhiteSpace)
854     {
855       // Select the word before or after the white space
856
857       if(0 == hitCharacter)
858       {
859         data.isWhiteSpace = false;
860         FindEndOfWord(data);
861         endIndex = data.foundIndex;
862       }
863       else if(hitCharacter > 0)
864       {
865         // Find the start of the word.
866         data.hitCharacter = hitCharacter - 1u;
867         data.isWhiteSpace = false;
868         FindStartOfWord(data);
869         startIndex = data.foundIndex;
870
871         --endIndex;
872       }
873     }
874   }
875
876   return matchedCharacter;
877 }
878
879 } // namespace Text
880
881 } // namespace Toolkit
882
883 } // namespace Dali