X-Git-Url: http://review.tizen.org/git/?p=platform%2Fcore%2Fuifw%2Fdali-toolkit.git;a=blobdiff_plain;f=dali-toolkit%2Finternal%2Ftext%2Fcursor-helper-functions.cpp;h=c70d530c3577705f43dc6f7f67737db33213b349;hp=1bce75a5d47fb53dd7e46d7114ac7c6348a1eb75;hb=2512aa2794f3c5ea5841542b9ed9c8c32973540b;hpb=43ecd3e46f0c6a6ad606e2d919cf6d8a20e70626 diff --git a/dali-toolkit/internal/text/cursor-helper-functions.cpp b/dali-toolkit/internal/text/cursor-helper-functions.cpp index 1bce75a..c70d530 100644 --- a/dali-toolkit/internal/text/cursor-helper-functions.cpp +++ b/dali-toolkit/internal/text/cursor-helper-functions.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 Samsung Electronics Co., Ltd. + * Copyright (c) 2022 Samsung Electronics Co., Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,75 +26,216 @@ namespace { - #if defined(DEBUG_ENABLED) - Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS"); +Debug::Filter* gLogFilter = Debug::Filter::New(Debug::NoLogging, true, "LOG_TEXT_CONTROLS"); #endif -} // namespace +const Dali::Toolkit::Text::CharacterDirection LTR = false; ///< Left To Right direction. -namespace Dali +struct FindWordData { + FindWordData(const Dali::Toolkit::Text::Character* const textBuffer, + Dali::Toolkit::Text::Length totalNumberOfCharacters, + Dali::Toolkit::Text::CharacterIndex hitCharacter, + bool isWhiteSpace, + bool isNewParagraph) + : textBuffer(textBuffer), + totalNumberOfCharacters(totalNumberOfCharacters), + hitCharacter(hitCharacter), + foundIndex(0), + isWhiteSpace(isWhiteSpace), + isNewParagraph(isNewParagraph) + { + } -namespace Toolkit + ~FindWordData() + { + } + + const Dali::Toolkit::Text::Character* const textBuffer; + Dali::Toolkit::Text::Length totalNumberOfCharacters; + Dali::Toolkit::Text::CharacterIndex hitCharacter; + Dali::Toolkit::Text::CharacterIndex foundIndex; + bool isWhiteSpace : 1u; + bool isNewParagraph : 1u; +}; + +bool IsWhiteSpaceOrNewParagraph(Dali::Toolkit::Text::Character character, + bool isHitWhiteSpace, + bool isHitWhiteSpaceOrNewParagraph) { + bool isWhiteSpaceOrNewParagraph = false; + if(isHitWhiteSpaceOrNewParagraph) + { + if(isHitWhiteSpace) + { + // Whether the current character is a white space. Note a new paragraph character is a white space as well but here is not wanted. + isWhiteSpaceOrNewParagraph = Dali::TextAbstraction::IsWhiteSpace(character) && !Dali::TextAbstraction::IsNewParagraph(character); + } + else + { + // Whether the current character is a new paragraph character. + isWhiteSpaceOrNewParagraph = Dali::TextAbstraction::IsNewParagraph(character); + } + } + else + { + // Whether the current character is a white space or a new paragraph character (note the new paragraph character is a white space as well). + isWhiteSpaceOrNewParagraph = Dali::TextAbstraction::IsWhiteSpace(character); + } -namespace Text + return isWhiteSpaceOrNewParagraph; +} + +void FindStartOfWord(FindWordData& data) +{ + const bool isHitWhiteSpaceOrNewParagraph = data.isWhiteSpace || data.isNewParagraph; + + for(data.foundIndex = data.hitCharacter; data.foundIndex > 0; --data.foundIndex) + { + const Dali::Toolkit::Text::Character character = *(data.textBuffer + data.foundIndex - 1u); + + const bool isWhiteSpaceOrNewParagraph = IsWhiteSpaceOrNewParagraph(character, + data.isWhiteSpace, + isHitWhiteSpaceOrNewParagraph); + + if(isHitWhiteSpaceOrNewParagraph != isWhiteSpaceOrNewParagraph) + { + break; + } + } +} + +void FindEndOfWord(FindWordData& data) { + const bool isHitWhiteSpaceOrNewParagraph = data.isWhiteSpace || data.isNewParagraph; -LineIndex GetClosestLine( VisualModelPtr visualModel, - float visualY ) + for(data.foundIndex = data.hitCharacter + 1u; data.foundIndex < data.totalNumberOfCharacters; ++data.foundIndex) + { + const Dali::Toolkit::Text::Character character = *(data.textBuffer + data.foundIndex); + + const bool isWhiteSpaceOrNewParagraph = IsWhiteSpaceOrNewParagraph(character, + data.isWhiteSpace, + isHitWhiteSpaceOrNewParagraph); + + if(isHitWhiteSpaceOrNewParagraph != isWhiteSpaceOrNewParagraph) + { + break; + } + } +} + +} //namespace + +namespace Dali +{ +namespace Toolkit { - float totalHeight = 0.f; - LineIndex lineIndex = 0u; +namespace Text +{ +LineIndex GetClosestLine(VisualModelPtr visualModel, + float visualY, + bool& matchedLine) +{ + float totalHeight = 0.f; + LineIndex lineIndex = 0; + matchedLine = false; + + if(visualY < 0.f) + { + return 0; + } const Vector& lines = visualModel->mLines; - for( LineIndex endLine = lines.Count(); - lineIndex < endLine; - ++lineIndex ) + + for(Vector::ConstIterator it = lines.Begin(), + endIt = lines.End(); + it != endIt; + ++it, ++lineIndex) { - const LineRun& lineRun = lines[lineIndex]; - totalHeight += lineRun.ascender + -lineRun.descender; - if( visualY < totalHeight ) + const LineRun& lineRun = *it; + + totalHeight += GetLineHeight(lineRun); + + if(visualY < totalHeight) { + matchedLine = true; return lineIndex; } } - if( lineIndex == 0 ) + if(lineIndex == 0) { return 0; } - return lineIndex-1; + return lineIndex - 1u; } -CharacterIndex GetClosestCursorIndex( VisualModelPtr visualModel, - LogicalModelPtr logicalModel, - MetricsPtr metrics, - float visualX, - float visualY ) +float CalculateLineOffset(const Vector& lines, + LineIndex lineIndex) { - DALI_LOG_INFO( gLogFilter, Debug::Verbose, "GetClosestCursorIndex, closest visualX %f visualY %f\n", visualX, visualY ); + float offset = 0.f; - CharacterIndex logicalIndex = 0u; + for(Vector::ConstIterator it = lines.Begin(), + endIt = lines.Begin() + lineIndex; + it != endIt; + ++it) + { + const LineRun& lineRun = *it; + + offset += GetLineHeight(lineRun); + } + + return offset; +} + +CharacterIndex GetClosestCursorIndex(VisualModelPtr visualModel, + LogicalModelPtr logicalModel, + MetricsPtr metrics, + float visualX, + float visualY, + CharacterHitTest::Mode mode, + bool& matchedCharacter) +{ + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "GetClosestCursorIndex, closest visualX %f visualY %f\n", visualX, visualY); + + // Whether there is a hit on a glyph. + matchedCharacter = false; - const Length numberOfGlyphs = visualModel->mGlyphs.Count(); - const Length numberOfLines = visualModel->mLines.Count(); - if( ( 0 == numberOfGlyphs ) || - ( 0 == numberOfLines ) ) + CharacterIndex logicalIndex = 0; + + const Length totalNumberOfGlyphs = visualModel->mGlyphs.Count(); + const Length totalNumberOfLines = visualModel->mLines.Count(); + if((0 == totalNumberOfGlyphs) || + (0 == totalNumberOfLines)) { return logicalIndex; } + const float characterSpacing = visualModel->GetCharacterSpacing(); + + // Whether there is a hit on a line. + bool matchedLine = false; // Find which line is closest. - const LineIndex lineIndex = Text::GetClosestLine( visualModel, - visualY ); - const LineRun& line = visualModel->mLines[lineIndex]; + const LineIndex lineIndex = Text::GetClosestLine(visualModel, + visualY, + matchedLine); + + if(!matchedLine && (CharacterHitTest::TAP == mode)) + { + // Return the first or the last character if the touch point doesn't hit a line. + return (visualY < 0.f) ? 0 : logicalModel->mText.Count(); + } + + // Convert from text's coords to line's coords. + const LineRun& line = *(visualModel->mLines.Begin() + lineIndex); + + // Transform the tap point from text's coords to line's coords. + visualX -= line.alignmentOffset; // Get the positions of the glyphs. - const Vector& positions = visualModel->mGlyphPositions; - const Vector2* const positionsBuffer = positions.Begin(); + const Vector2* const positionsBuffer = visualModel->mGlyphPositions.Begin(); // Get the character to glyph conversion table. const GlyphIndex* const charactersToGlyphBuffer = visualModel->mCharactersToGlyph.Begin(); @@ -102,341 +243,623 @@ CharacterIndex GetClosestCursorIndex( VisualModelPtr visualModel, // Get the glyphs per character table. const Length* const glyphsPerCharacterBuffer = visualModel->mGlyphsPerCharacter.Begin(); + // Get the characters per glyph table. + const Length* const charactersPerGlyphBuffer = visualModel->mCharactersPerGlyph.Begin(); + // Get the glyph's info buffer. const GlyphInfo* const glyphInfoBuffer = visualModel->mGlyphs.Begin(); const CharacterIndex startCharacter = line.characterRun.characterIndex; const CharacterIndex endCharacter = line.characterRun.characterIndex + line.characterRun.numberOfCharacters; - DALI_ASSERT_DEBUG( endCharacter <= logicalModel->mText.Count() && "Invalid line info" ); + DALI_ASSERT_DEBUG(endCharacter <= logicalModel->mText.Count() && "Invalid line info"); // Whether this line is a bidirectional line. - const bool bidiLineFetched = logicalModel->FetchBidirectionalLineInfo( startCharacter ); + const bool bidiLineFetched = logicalModel->FetchBidirectionalLineInfo(startCharacter); - // Whether there is a hit on a glyph. - bool matched = false; + // The character's direction buffer. + const CharacterDirection* const directionsBuffer = bidiLineFetched ? logicalModel->mCharacterDirections.Begin() : NULL; + + // Whether the touch point if before the first glyph. + bool isBeforeFirstGlyph = false; // Traverses glyphs in visual order. To do that use the visual to logical conversion table. - CharacterIndex visualIndex = startCharacter; - Length numberOfCharacters = 0u; - for( ; !matched && ( visualIndex < endCharacter ); ++visualIndex ) + CharacterIndex visualIndex = startCharacter; + Length numberOfVisualCharacters = 0; + float calculatedAdvance = 0.f; + Vector& glyphToCharacterMap = visualModel->mGlyphsToCharacters; + const CharacterIndex* glyphToCharacterMapBuffer = glyphToCharacterMap.Begin(); + for(; visualIndex < endCharacter; ++visualIndex) { // The character in logical order. - const CharacterIndex characterLogicalOrderIndex = ( bidiLineFetched ? logicalModel->GetLogicalCharacterIndex( visualIndex ) : visualIndex ); - - // Get the script of the character. - const Script script = logicalModel->GetScript( characterLogicalOrderIndex ); + const CharacterIndex characterLogicalOrderIndex = (bidiLineFetched ? logicalModel->GetLogicalCharacterIndex(visualIndex) : visualIndex); + const CharacterDirection direction = (bidiLineFetched ? *(directionsBuffer + characterLogicalOrderIndex) : LTR); // The number of glyphs for that character - const Length numberOfGlyphs = *( glyphsPerCharacterBuffer + characterLogicalOrderIndex ); - ++numberOfCharacters; + const Length numberOfGlyphs = *(glyphsPerCharacterBuffer + characterLogicalOrderIndex); + ++numberOfVisualCharacters; - - if( 0u != numberOfGlyphs ) + if(0 != numberOfGlyphs) { // Get the first character/glyph of the group of glyphs. - const CharacterIndex firstVisualCharacterIndex = 1u + visualIndex - numberOfCharacters; - const CharacterIndex firstLogicalCharacterIndex = ( bidiLineFetched ? logicalModel->GetLogicalCharacterIndex( firstVisualCharacterIndex ) : firstVisualCharacterIndex ); - const GlyphIndex firstLogicalGlyphIndex = *( charactersToGlyphBuffer + firstLogicalCharacterIndex ); + const CharacterIndex firstVisualCharacterIndex = 1u + visualIndex - numberOfVisualCharacters; + const CharacterIndex firstLogicalCharacterIndex = (bidiLineFetched ? logicalModel->GetLogicalCharacterIndex(firstVisualCharacterIndex) : firstVisualCharacterIndex); + const GlyphIndex firstLogicalGlyphIndex = *(charactersToGlyphBuffer + firstLogicalCharacterIndex); // Get the metrics for the group of glyphs. GlyphMetrics glyphMetrics; - GetGlyphsMetrics( firstLogicalGlyphIndex, - numberOfGlyphs, - glyphMetrics, - glyphInfoBuffer, - metrics ); + calculatedAdvance = GetCalculatedAdvance(*(logicalModel->mText.Begin() + (*(glyphToCharacterMapBuffer + firstLogicalGlyphIndex))), characterSpacing, (*(visualModel->mGlyphs.Begin() + firstLogicalGlyphIndex)).advance); + GetGlyphsMetrics(firstLogicalGlyphIndex, + numberOfGlyphs, + glyphMetrics, + glyphInfoBuffer, + metrics, + calculatedAdvance); // Get the position of the first glyph. - const Vector2& position = *( positionsBuffer + firstLogicalGlyphIndex ); + const Vector2& position = *(positionsBuffer + firstLogicalGlyphIndex); + + if(startCharacter == visualIndex) + { + const float glyphPosition = -glyphMetrics.xBearing + position.x; + + if(visualX < glyphPosition) + { + isBeforeFirstGlyph = true; + break; + } + } + + // Whether the glyph can be split, like Latin ligatures fi, ff or Arabic (ل + ا). + Length numberOfCharacters = *(charactersPerGlyphBuffer + firstLogicalGlyphIndex); + if(direction != LTR) + { + // As characters are being traversed in visual order, + // for right to left ligatures, the character which contains the + // number of glyphs in the table is found first. + // Jump the number of characters to the next glyph is needed. + + if(0 == numberOfCharacters) + { + // TODO: This is a workaround to fix an issue with complex characters in the arabic + // script like i.e. رّ or الأَبْجَدِيَّة العَرَبِيَّة + // There are characters that are not shaped in one glyph but in combination with + // the next one generates two of them. + // The visual to logical conversion table have characters in different order than + // expected even if all of them are arabic. + + // The workaround doesn't fix the issue completely but it prevents the application + // to hang in an infinite loop. + + // Find the number of characters. + for(GlyphIndex index = firstLogicalGlyphIndex + 1u; + (0 == numberOfCharacters) && (index < totalNumberOfGlyphs); + ++index) + { + numberOfCharacters = *(charactersPerGlyphBuffer + index); + } + + if(2u > numberOfCharacters) + { + continue; + } + + --numberOfCharacters; + } + + visualIndex += numberOfCharacters - 1u; + } - // Whether the glyph can be split, like Latin ligatures fi, ff or Arabic ﻻ. - const bool isInterglyphIndex = ( numberOfCharacters > numberOfGlyphs ) && HasLigatureMustBreak( script ); - const Length numberOfBlocks = isInterglyphIndex ? numberOfCharacters : 1u; - const float glyphAdvance = glyphMetrics.advance / static_cast( numberOfBlocks ); + // Get the script of the character. + const Script script = logicalModel->GetScript(characterLogicalOrderIndex); - GlyphIndex index = 0u; - for( ; !matched && ( index < numberOfBlocks ); ++index ) + const bool isInterglyphIndex = (numberOfCharacters > numberOfGlyphs) && HasLigatureMustBreak(script); + const Length numberOfBlocks = isInterglyphIndex ? numberOfCharacters : 1u; + const float glyphAdvance = glyphMetrics.advance / static_cast(numberOfBlocks); + + CharacterIndex index = 0; + for(; index < numberOfBlocks; ++index) { // Find the mid-point of the area containing the glyph - const float glyphCenter = -glyphMetrics.xBearing + position.x + ( static_cast( index ) + 0.5f ) * glyphAdvance; + const float glyphCenter = -glyphMetrics.xBearing + position.x + (static_cast(index) + 0.5f) * glyphAdvance; - if( visualX < glyphCenter ) + if(visualX < glyphCenter) { - matched = true; + matchedCharacter = true; break; } } - if( matched ) + if(matchedCharacter) { + // If the glyph is shaped from more than one character, it matches the character of the glyph. visualIndex = firstVisualCharacterIndex + index; break; } - numberOfCharacters = 0u; + numberOfVisualCharacters = 0; } + } // for characters in visual order. - } + // The number of characters of the whole text. + const Length totalNumberOfCharacters = logicalModel->mText.Count(); // Return the logical position of the cursor in characters. - if( !matched ) + if(!matchedCharacter) { - visualIndex = endCharacter; + if(isBeforeFirstGlyph) + { + // If no character is matched, then the first character (in visual order) of the line is used. + visualIndex = startCharacter; + } + else + { + // If no character is matched, then the last character (in visual order) of the line is used. + visualIndex = endCharacter; + } } - logicalIndex = ( bidiLineFetched ? logicalModel->GetLogicalCursorIndex( visualIndex ) : visualIndex ); - DALI_LOG_INFO( gLogFilter, Debug::Verbose, "closest visualIndex %d logicalIndex %d\n", visualIndex, logicalIndex ); + // Get the paragraph direction. + const CharacterDirection paragraphDirection = line.direction; - DALI_ASSERT_DEBUG( ( logicalIndex <= logicalModel->mText.Count() && logicalIndex >= 0 ) && "GetClosestCursorIndex - Out of bounds index" ); + if(totalNumberOfCharacters != visualIndex) + { + // The visual index is not at the end of the text. - return logicalIndex; -} + if(LTR == paragraphDirection) + { + // The paragraph direction is left to right. + if(visualIndex == endCharacter) + { + // It places the cursor just before the last character in visual order. + // i.e. it places the cursor just before the '\n' or before the last character + // if there is a long line with no word breaks which is wrapped. + + // It doesn't check if the closest line is the last one like the RTL branch below + // because the total number of characters is different than the visual index and + // the visual index is the last character of the line. + --visualIndex; + } + } + else + { + // The paragraph direction is right to left. -void GetCursorPosition( VisualModelPtr visualModel, - LogicalModelPtr logicalModel, - MetricsPtr metrics, - CharacterIndex logical, - CursorInfo& cursorInfo ) -{ - // TODO: Check for multiline with \n, etc... + if((lineIndex != totalNumberOfLines - 1u) && // is not the last line. + (visualIndex == startCharacter)) + { + // It places the cursor just after the first character in visual order. + // i.e. it places the cursor just after the '\n' or after the last character + // if there is a long line with no word breaks which is wrapped. - const Length numberOfCharacters = logicalModel->mText.Count(); + // If the last line doesn't end with '\n' it won't increase the visual index + // placing the cursor at the beginning of the line (in visual order). + ++visualIndex; + } + } + } + else + { + // The visual index is at the end of text. - // Check if the logical position is the first or the last one of the text. - const bool isFirstPosition = 0u == logical; - const bool isLastPosition = numberOfCharacters == logical; + // 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. + // This branch checks if the closest line is the one with the last '\n'. If it is, it decrements the visual index to place + // the cursor just before the last '\n'. - // 'logical' is the logical 'cursor' index. - // Get the next and current logical 'character' index. - const CharacterIndex nextCharacterIndex = logical; - const CharacterIndex characterIndex = isFirstPosition ? logical : logical - 1u; + if((lineIndex != totalNumberOfLines - 1u) && + TextAbstraction::IsNewParagraph(*(logicalModel->mText.Begin() + visualIndex - 1u))) + { + --visualIndex; + } + } + + logicalIndex = (bidiLineFetched ? logicalModel->GetLogicalCursorIndex(visualIndex) : visualIndex); + + DALI_LOG_INFO(gLogFilter, Debug::Verbose, "closest visualIndex %d logicalIndex %d\n", visualIndex, logicalIndex); + + DALI_ASSERT_DEBUG((logicalIndex <= logicalModel->mText.Count() && logicalIndex >= 0) && "GetClosestCursorIndex - Out of bounds index"); - // Get the direction of the character and the next one. - const CharacterDirection* const modelCharacterDirectionsBuffer = ( 0u != logicalModel->mCharacterDirections.Count() ) ? logicalModel->mCharacterDirections.Begin() : NULL; + return logicalIndex; +} - CharacterDirection isCurrentRightToLeft = false; - CharacterDirection isNextRightToLeft = false; - if( NULL != modelCharacterDirectionsBuffer ) // If modelCharacterDirectionsBuffer is NULL, it means the whole text is left to right. +void GetCursorPosition(GetCursorPositionParameters& parameters, + float defaultFontLineHeight, + CursorInfo& cursorInfo) +{ + const LineRun* const modelLines = parameters.visualModel->mLines.Begin(); + if(NULL == modelLines) { - isCurrentRightToLeft = *( modelCharacterDirectionsBuffer + characterIndex ); - isNextRightToLeft = *( modelCharacterDirectionsBuffer + nextCharacterIndex ); + // Nothing to do. + return; } + // Whether the logical cursor position is at the end of the whole text. + const bool isLastPosition = parameters.logicalModel->mText.Count() == parameters.logical; + // Get the line where the character is laid-out. - const LineRun* const modelLines = visualModel->mLines.Begin(); + const CharacterIndex characterOfLine = isLastPosition ? (parameters.logical - 1u) : parameters.logical; - const LineIndex lineIndex = visualModel->GetLineOfCharacter( characterIndex ); - const LineRun& line = *( modelLines + lineIndex ); + // Whether the cursor is in the last position and the last position is a new paragraph character. + const bool isLastNewParagraph = parameters.isMultiline && isLastPosition && TextAbstraction::IsNewParagraph(*(parameters.logicalModel->mText.Begin() + characterOfLine)); - // Get the paragraph's direction. - const CharacterDirection isRightToLeftParagraph = line.direction; + const LineIndex lineIndex = parameters.visualModel->GetLineOfCharacter(characterOfLine); + const LineRun& line = *(modelLines + lineIndex); - // Check whether there is an alternative position: + CharacterIndex index; + GlyphMetrics glyphMetrics; + MetricsPtr& metrics = parameters.metrics; + GlyphIndex glyphIndex = 0u; + Length numberOfGlyphs = 0u; - cursorInfo.isSecondaryCursor = ( !isLastPosition && ( isCurrentRightToLeft != isNextRightToLeft ) ) || - ( isLastPosition && ( isRightToLeftParagraph != isCurrentRightToLeft ) ); + if(isLastNewParagraph) + { + // The cursor is in a new line with no characters. Place the cursor in that line. + const LineIndex newLineIndex = lineIndex + 1u; + const LineRun& newLine = *(modelLines + newLineIndex); - // Set the line offset and height. - cursorInfo.lineOffset = 0.f; - cursorInfo.lineHeight = line.ascender + -line.descender; + cursorInfo.isSecondaryCursor = false; - // Calculate the primary cursor. + // Set the line offset and height. + cursorInfo.lineOffset = CalculateLineOffset(parameters.visualModel->mLines, + newLineIndex); - CharacterIndex index = characterIndex; - if( cursorInfo.isSecondaryCursor ) - { - // If there is a secondary position, the primary cursor may be in a different place than the logical index. + cursorInfo.lineHeight = GetLineHeight(newLine); - if( isLastPosition ) + index = 0u; + const Length totalNumberOfCharacters = parameters.logicalModel->mText.Count(); + if(totalNumberOfCharacters > 0u) { - // The position of the cursor after the last character needs special - // care depending on its direction and the direction of the paragraph. + index = totalNumberOfCharacters - 1u; + } - // Need to find the first character after the last character with the paragraph's direction. - // i.e l0 l1 l2 r0 r1 should find r0. + GetGlyphMetricsFromCharacterIndex(index, parameters.visualModel, parameters.logicalModel, metrics, glyphMetrics, glyphIndex, numberOfGlyphs); - // TODO: check for more than one line! - index = isRightToLeftParagraph ? line.characterRun.characterIndex : line.characterRun.characterIndex + line.characterRun.numberOfCharacters - 1u; - index = logicalModel->GetLogicalCharacterIndex( index ); - } - else - { - index = ( isRightToLeftParagraph == isCurrentRightToLeft ) ? characterIndex : nextCharacterIndex; - } + // Set the primary cursor's height. + // 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. + cursorInfo.primaryCursorHeight = (totalNumberOfCharacters > 0) ? (cursorInfo.isSecondaryCursor ? 0.5f * glyphMetrics.fontHeight : glyphMetrics.fontHeight) : defaultFontLineHeight; + + // Set the primary cursor's position. + cursorInfo.primaryPosition.x = (LTR == line.direction) ? newLine.alignmentOffset : parameters.visualModel->mControlSize.width - newLine.alignmentOffset; + cursorInfo.primaryPosition.y = cursorInfo.lineOffset; } + else + { + // Whether this line is a bidirectional line. + const bool bidiLineFetched = parameters.logicalModel->FetchBidirectionalLineInfo(characterOfLine); - const GlyphIndex* const charactersToGlyphBuffer = visualModel->mCharactersToGlyph.Begin(); - const Length* const glyphsPerCharacterBuffer = visualModel->mGlyphsPerCharacter.Begin(); - const Length* const charactersPerGlyphBuffer = visualModel->mCharactersPerGlyph.Begin(); - const CharacterIndex* const glyphsToCharactersBuffer = visualModel->mGlyphsToCharacters.Begin(); - const Vector2* const glyphPositionsBuffer = visualModel->mGlyphPositions.Begin(); - const GlyphInfo* const glyphInfoBuffer = visualModel->mGlyphs.Begin(); + // Check if the logical position is the first or the last one of the line. + const bool isFirstPositionOfLine = line.characterRun.characterIndex == parameters.logical; + const bool isLastPositionOfLine = line.characterRun.characterIndex + line.characterRun.numberOfCharacters == parameters.logical; - // Convert the cursor position into the glyph position. - const GlyphIndex primaryGlyphIndex = *( charactersToGlyphBuffer + index ); - const Length primaryNumberOfGlyphs = *( glyphsPerCharacterBuffer + index ); - const Length primaryNumberOfCharacters = *( charactersPerGlyphBuffer + primaryGlyphIndex ); - - // Get the metrics for the group of glyphs. - GlyphMetrics glyphMetrics; - GetGlyphsMetrics( primaryGlyphIndex, - primaryNumberOfGlyphs, - glyphMetrics, - glyphInfoBuffer, - metrics ); - - // Whether to add the glyph's advance to the cursor position. - // 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, - // if the logical cursor is one, the position is the position of the first glyph and the advance is added. - // A 'truth table' was build and an online Karnaugh map tool was used to simplify the logic. - // - // FLCP A - // ------ - // 0000 1 - // 0001 1 - // 0010 0 - // 0011 0 - // 0100 1 - // 0101 0 - // 0110 1 - // 0111 0 - // 1000 0 - // 1001 x - // 1010 x - // 1011 1 - // 1100 x - // 1101 x - // 1110 x - // 1111 x - // - // Where F -> isFirstPosition - // L -> isLastPosition - // C -> isCurrentRightToLeft - // P -> isRightToLeftParagraph - // A -> Whether to add the glyph's advance. - - const bool addGlyphAdvance = ( ( isLastPosition && !isRightToLeftParagraph ) || - ( isFirstPosition && isRightToLeftParagraph ) || - ( !isFirstPosition && !isLastPosition && !isCurrentRightToLeft ) ); - - float glyphAdvance = addGlyphAdvance ? glyphMetrics.advance : 0.f; - - if( !isLastPosition && - ( primaryNumberOfCharacters > 1u ) ) - { - const CharacterIndex firstIndex = *( glyphsToCharactersBuffer + primaryGlyphIndex ); + // 'logical' is the logical 'cursor' index. + // Get the next and current logical 'character' index. + const CharacterIndex characterIndex = isFirstPositionOfLine ? parameters.logical : parameters.logical - 1u; + const CharacterIndex nextCharacterIndex = isLastPositionOfLine ? characterIndex : parameters.logical; - bool isCurrentRightToLeft = false; - if( NULL != modelCharacterDirectionsBuffer ) // If modelCharacterDirectionsBuffer is NULL, it means the whole text is left to right. - { - isCurrentRightToLeft = *( modelCharacterDirectionsBuffer + index ); - } + // The character's direction buffer. + const CharacterDirection* const directionsBuffer = bidiLineFetched ? parameters.logicalModel->mCharacterDirections.Begin() : NULL; - Length numberOfGlyphAdvance = ( isFirstPosition ? 0u : 1u ) + characterIndex - firstIndex; - if( isCurrentRightToLeft ) + CharacterDirection isCurrentRightToLeft = false; + CharacterDirection isNextRightToLeft = false; + if(bidiLineFetched) // If bidiLineFetched is false, it means the whole text is left to right. { - numberOfGlyphAdvance = primaryNumberOfCharacters - numberOfGlyphAdvance; + isCurrentRightToLeft = *(directionsBuffer + characterIndex); + isNextRightToLeft = *(directionsBuffer + nextCharacterIndex); } - glyphAdvance = static_cast( numberOfGlyphAdvance ) * glyphMetrics.advance / static_cast( primaryNumberOfCharacters ); - } + // Get the paragraph's direction. + const CharacterDirection isRightToLeftParagraph = line.direction; - // Get the glyph position and x bearing. - const Vector2& primaryPosition = *( glyphPositionsBuffer + primaryGlyphIndex ); + // Check whether there is an alternative position: + cursorInfo.isSecondaryCursor = ((!isLastPositionOfLine && (isCurrentRightToLeft != isNextRightToLeft)) || + (isLastPositionOfLine && (isRightToLeftParagraph != isCurrentRightToLeft)) || + (isFirstPositionOfLine && (isRightToLeftParagraph != isCurrentRightToLeft))); - // Set the primary cursor's height. - cursorInfo.primaryCursorHeight = cursorInfo.isSecondaryCursor ? 0.5f * glyphMetrics.fontHeight : glyphMetrics.fontHeight; + // Set the line offset and height. + cursorInfo.lineOffset = CalculateLineOffset(parameters.visualModel->mLines, + lineIndex); - // Set the primary cursor's position. - cursorInfo.primaryPosition.x = -glyphMetrics.xBearing + primaryPosition.x + glyphAdvance; - cursorInfo.primaryPosition.y = line.ascender - glyphMetrics.ascender; + cursorInfo.lineHeight = GetLineHeight(line); - // Calculate the secondary cursor. + // Calculate the primary cursor. - if( cursorInfo.isSecondaryCursor ) - { - // Set the secondary cursor's height. - cursorInfo.secondaryCursorHeight = 0.5f * glyphMetrics.fontHeight; + index = characterIndex; + if(cursorInfo.isSecondaryCursor) + { + // If there is a secondary position, the primary cursor may be in a different place than the logical index. + + if(isLastPositionOfLine) + { + // The position of the cursor after the last character needs special + // care depending on its direction and the direction of the paragraph. - CharacterIndex index = characterIndex; - if( !isLastPosition ) + // Need to find the first character after the last character with the paragraph's direction. + // i.e l0 l1 l2 r0 r1 should find r0. + + index = isRightToLeftParagraph ? line.characterRun.characterIndex : line.characterRun.characterIndex + line.characterRun.numberOfCharacters - 1u; + if(bidiLineFetched) + { + index = parameters.logicalModel->GetLogicalCharacterIndex(index); + } + } + else if(isFirstPositionOfLine) + { + index = isRightToLeftParagraph ? line.characterRun.characterIndex + line.characterRun.numberOfCharacters - 1u : line.characterRun.characterIndex; + if(bidiLineFetched) + { + index = parameters.logicalModel->GetLogicalCharacterIndex(index); + } + } + else + { + index = (isRightToLeftParagraph == isCurrentRightToLeft) ? characterIndex : nextCharacterIndex; + } + } + + const Length* const charactersPerGlyphBuffer = parameters.visualModel->mCharactersPerGlyph.Begin(); + const CharacterIndex* const glyphsToCharactersBuffer = parameters.visualModel->mGlyphsToCharacters.Begin(); + const Vector2* const glyphPositionsBuffer = parameters.visualModel->mGlyphPositions.Begin(); + const float characterSpacing = parameters.visualModel->GetCharacterSpacing(); + + // Get the metrics for the group of glyphs. + GetGlyphMetricsFromCharacterIndex(index, parameters.visualModel, parameters.logicalModel, metrics, glyphMetrics, glyphIndex, numberOfGlyphs); + + // Convert the cursor position into the glyph position. + const GlyphIndex primaryGlyphIndex = glyphIndex; + const Length primaryNumberOfCharacters = *(charactersPerGlyphBuffer + primaryGlyphIndex); + + // Whether to add the glyph's advance to the cursor position. + // 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, + // if the logical cursor is one, the position is the position of the first glyph and the advance is added. + // A 'truth table' was build and an online Karnaugh map tool was used to simplify the logic. + // + // FLCP A + // ------ + // 0000 1 + // 0001 1 + // 0010 0 + // 0011 0 + // 0100 1 + // 0101 0 + // 0110 1 + // 0111 0 + // 1000 0 + // 1001 1 + // 1010 0 + // 1011 1 + // 1100 x + // 1101 x + // 1110 x + // 1111 x + // + // Where F -> isFirstPosition + // L -> isLastPosition + // C -> isCurrentRightToLeft + // P -> isRightToLeftParagraph + // A -> Whether to add the glyph's advance. + + const bool addGlyphAdvance = ((isLastPositionOfLine && !isRightToLeftParagraph) || + (isFirstPositionOfLine && isRightToLeftParagraph) || + (!isFirstPositionOfLine && !isLastPosition && !isCurrentRightToLeft)); + + float glyphAdvance = addGlyphAdvance ? (glyphMetrics.advance) : 0.f; + + if(!isLastPositionOfLine && + (primaryNumberOfCharacters > 1u)) { - index = ( isRightToLeftParagraph == isCurrentRightToLeft ) ? nextCharacterIndex : characterIndex; + const CharacterIndex firstIndex = *(glyphsToCharactersBuffer + primaryGlyphIndex); + + bool isCurrentRightToLeft = false; + if(bidiLineFetched) // If bidiLineFetched is false, it means the whole text is left to right. + { + isCurrentRightToLeft = *(directionsBuffer + index); + } + + Length numberOfGlyphAdvance = (isFirstPositionOfLine ? 0 : 1u) + characterIndex - firstIndex; + if(isCurrentRightToLeft) + { + numberOfGlyphAdvance = primaryNumberOfCharacters - numberOfGlyphAdvance; + } + + glyphAdvance = static_cast(numberOfGlyphAdvance) * (glyphMetrics.advance) / static_cast(primaryNumberOfCharacters); } - const GlyphIndex secondaryGlyphIndex = *( charactersToGlyphBuffer + index ); - const Length secondaryNumberOfGlyphs = *( glyphsPerCharacterBuffer + index ); + // Get the glyph position and x bearing (in the line's coords). + const Vector2& primaryPosition = *(glyphPositionsBuffer + primaryGlyphIndex); - const Vector2& secondaryPosition = *( glyphPositionsBuffer + secondaryGlyphIndex ); + // Set the primary cursor's height. + cursorInfo.primaryCursorHeight = cursorInfo.isSecondaryCursor ? 0.5f * glyphMetrics.fontHeight : glyphMetrics.fontHeight; - GetGlyphsMetrics( secondaryGlyphIndex, - secondaryNumberOfGlyphs, - glyphMetrics, - glyphInfoBuffer, - metrics ); + cursorInfo.glyphOffset = line.ascender - glyphMetrics.ascender; + // Set the primary cursor's position. + cursorInfo.primaryPosition.x = -glyphMetrics.xBearing + primaryPosition.x + glyphAdvance; + cursorInfo.primaryPosition.y = cursorInfo.lineOffset + cursorInfo.glyphOffset; - // Set the secondary cursor's position. - cursorInfo.secondaryPosition.x = -glyphMetrics.xBearing + secondaryPosition.x + ( isCurrentRightToLeft ? 0.f : glyphMetrics.advance ); - cursorInfo.secondaryPosition.y = cursorInfo.lineHeight - cursorInfo.secondaryCursorHeight - line.descender - ( glyphMetrics.fontHeight - glyphMetrics.ascender ); + // Transform the cursor info from line's coords to text's coords. + cursorInfo.primaryPosition.x += line.alignmentOffset; + + // Calculate the secondary cursor. + if(cursorInfo.isSecondaryCursor) + { + // Set the secondary cursor's height. + cursorInfo.secondaryCursorHeight = 0.5f * glyphMetrics.fontHeight; + + CharacterIndex index = characterIndex; + if(!isLastPositionOfLine) + { + index = (isRightToLeftParagraph == isCurrentRightToLeft) ? nextCharacterIndex : characterIndex; + } + + GetGlyphMetricsFromCharacterIndex(index, parameters.visualModel, parameters.logicalModel, metrics, glyphMetrics, glyphIndex, numberOfGlyphs); + + const GlyphIndex secondaryGlyphIndex = glyphIndex; + const Vector2& secondaryPosition = *(glyphPositionsBuffer + secondaryGlyphIndex); + + // Set the secondary cursor's position. + + // FCP A + // ------ + // 000 1 + // 001 x + // 010 0 + // 011 0 + // 100 x + // 101 0 + // 110 1 + // 111 x + // + // Where F -> isFirstPosition + // C -> isCurrentRightToLeft + // P -> isRightToLeftParagraph + // A -> Whether to add the glyph's advance. + + const bool addGlyphAdvance = ((!isFirstPositionOfLine && !isCurrentRightToLeft) || + (isFirstPositionOfLine && !isRightToLeftParagraph)); + + cursorInfo.secondaryPosition.x = -glyphMetrics.xBearing + secondaryPosition.x + (addGlyphAdvance ? (glyphMetrics.advance + characterSpacing) : 0.f); + cursorInfo.secondaryPosition.y = cursorInfo.lineOffset + cursorInfo.lineHeight - cursorInfo.secondaryCursorHeight; + + // Transform the cursor info from line's coords to text's coords. + cursorInfo.secondaryPosition.x += line.alignmentOffset; + } } } -void FindSelectionIndices( VisualModelPtr visualModel, - LogicalModelPtr logicalModel, - MetricsPtr metrics, - float visualX, - float visualY, - CharacterIndex& startIndex, - CharacterIndex& endIndex ) +bool FindSelectionIndices(VisualModelPtr visualModel, + LogicalModelPtr logicalModel, + MetricsPtr metrics, + float visualX, + float visualY, + CharacterIndex& startIndex, + CharacterIndex& endIndex, + CharacterIndex& noTextHitIndex) { - CharacterIndex hitCharacter = Text::GetClosestCursorIndex( visualModel, - logicalModel, - metrics, - visualX, - visualY ); - DALI_ASSERT_DEBUG( hitCharacter <= logicalModel->mText.Count() && "GetClosestCursorIndex returned out of bounds index" ); - - if( logicalModel->mText.Count() == 0 ) + /* + Hit character Select +|-------------------------------------------------------|------------------------------------------| +| On a word | The word | +| On a single white space between words | The word before or after the white space | +| On one of the multiple contiguous white spaces | The white spaces | +| On a single white space which is in the position zero | The white space and the next word | +| On a new paragraph character | The word or group of white spaces before | +|-------------------------------------------------------|------------------------------------------| +*/ + const Length totalNumberOfCharacters = logicalModel->mText.Count(); + startIndex = 0; + endIndex = 0; + noTextHitIndex = 0; + + if(0 == totalNumberOfCharacters) + { + // Nothing to do if the model is empty. + return false; + } + + bool matchedCharacter = false; + CharacterIndex hitCharacter = Text::GetClosestCursorIndex(visualModel, + logicalModel, + metrics, + visualX, + visualY, + CharacterHitTest::TAP, + matchedCharacter); + + if(!matchedCharacter) { - return; // if model empty + noTextHitIndex = hitCharacter; } - if( hitCharacter >= logicalModel->mText.Count() ) + DALI_ASSERT_DEBUG((hitCharacter <= totalNumberOfCharacters) && "GetClosestCursorIndex returned out of bounds index"); + + if(hitCharacter >= totalNumberOfCharacters) { // Closest hit character is the last character. - if( hitCharacter == logicalModel->mText.Count() ) + if(hitCharacter == totalNumberOfCharacters) { hitCharacter--; //Hit character index set to last character in logical model } else { // hitCharacter is out of bounds - return; + return false; } } + const Character* const textBuffer = logicalModel->mText.Begin(); + startIndex = hitCharacter; - endIndex = hitCharacter; - bool isHitCharacterWhitespace = TextAbstraction::IsWhiteSpace( logicalModel->mText[hitCharacter] ); + endIndex = hitCharacter; + + // Whether the hit character is a new paragraph character. + const bool isHitCharacterNewParagraph = TextAbstraction::IsNewParagraph(*(textBuffer + hitCharacter)); + + // Whether the hit character is a white space. Note a new paragraph character is a white space as well but here is not wanted. + const bool isHitCharacterWhiteSpace = TextAbstraction::IsWhiteSpace(*(textBuffer + hitCharacter)) && !isHitCharacterNewParagraph; - // Find the start and end of the text - for( startIndex = hitCharacter; startIndex > 0; --startIndex ) + FindWordData data(textBuffer, + totalNumberOfCharacters, + hitCharacter, + isHitCharacterWhiteSpace, + isHitCharacterNewParagraph); + + if(isHitCharacterNewParagraph) { - if( isHitCharacterWhitespace != TextAbstraction::IsWhiteSpace( logicalModel->mText[ startIndex-1 ] ) ) + // Find the first character before the hit one which is not a new paragraph character. + + if(hitCharacter > 0) { - break; + endIndex = hitCharacter - 1u; + for(; endIndex > 0; --endIndex) + { + const Dali::Toolkit::Text::Character character = *(data.textBuffer + endIndex); + + if(!Dali::TextAbstraction::IsNewParagraph(character)) + { + break; + } + } } + + data.hitCharacter = endIndex; + data.isNewParagraph = false; + data.isWhiteSpace = TextAbstraction::IsWhiteSpace(*(textBuffer + data.hitCharacter)); } - const CharacterIndex pastTheEnd = logicalModel->mText.Count(); - for( endIndex = hitCharacter + 1u; endIndex < pastTheEnd; ++endIndex ) + + // Find the start of the word. + FindStartOfWord(data); + startIndex = data.foundIndex; + + // Find the end of the word. + FindEndOfWord(data); + endIndex = data.foundIndex; + + if(1u == (endIndex - startIndex)) { - if( isHitCharacterWhitespace != TextAbstraction::IsWhiteSpace( logicalModel->mText[ endIndex ] ) ) + if(isHitCharacterWhiteSpace) { - break; + // Select the word before or after the white space + + if(0 == hitCharacter) + { + data.isWhiteSpace = false; + FindEndOfWord(data); + endIndex = data.foundIndex; + } + else if(hitCharacter > 0) + { + // Find the start of the word. + data.hitCharacter = hitCharacter - 1u; + data.isWhiteSpace = false; + FindStartOfWord(data); + startIndex = data.foundIndex; + + --endIndex; + } } } + + return matchedCharacter; } } // namespace Text