+ // Nothing else to do.
+ return;
+ }
+
+ // Get the previous logical index.
+ const CharacterIndex previousLogical = isFirstPosition ? 0u : logical - 1u;
+
+ // Decrease the logical index if it's the last one.
+ if( isLastPosition )
+ {
+ --logical;
+ }
+
+ // Get the direction of the character and the previous one.
+ const CharacterDirection* const modelCharacterDirectionsBuffer = ( 0u != mLogicalModel->mCharacterDirections.Count() ) ? mLogicalModel->mCharacterDirections.Begin() : NULL;
+
+ CharacterDirection isCurrentRightToLeft = false;
+ CharacterDirection isPreviousRightToLeft = false;
+ if( NULL != modelCharacterDirectionsBuffer ) // If modelCharacterDirectionsBuffer is NULL, it means the whole text is left to right.
+ {
+ isCurrentRightToLeft = *( modelCharacterDirectionsBuffer + logical );
+ isPreviousRightToLeft = *( modelCharacterDirectionsBuffer + previousLogical );
+ }
+
+ // Get the line where the character is laid-out.
+ const LineRun* modelLines = mVisualModel->mLines.Begin();
+
+ const LineIndex lineIndex = mVisualModel->GetLineOfCharacter( logical );
+ const LineRun& line = *( modelLines + lineIndex );
+
+ // Get the paragraph's direction.
+ const CharacterDirection isRightToLeftParagraph = line.direction;
+
+ // Check whether there is an alternative position:
+
+ cursorInfo.isSecondaryCursor = ( isCurrentRightToLeft != isPreviousRightToLeft ) ||
+ ( isLastPosition && ( isRightToLeftParagraph != isCurrentRightToLeft ) );
+
+ // Set the line height.
+ cursorInfo.lineHeight = line.ascender + -line.descender;
+
+ // Convert the cursor position into the glyph position.
+ CharacterIndex characterIndex = logical;
+ if( cursorInfo.isSecondaryCursor &&
+ ( isRightToLeftParagraph != isCurrentRightToLeft ) )
+ {
+ characterIndex = previousLogical;
+ }
+
+ const GlyphIndex currentGlyphIndex = *( mVisualModel->mCharactersToGlyph.Begin() + characterIndex );
+ const Length numberOfGlyphs = *( mVisualModel->mGlyphsPerCharacter.Begin() + characterIndex );
+ const Length numberOfCharacters = *( mVisualModel->mCharactersPerGlyph.Begin() +currentGlyphIndex );
+
+ // Get the metrics for the group of glyphs.
+ GlyphMetrics glyphMetrics;
+ GetGlyphsMetrics( currentGlyphIndex,
+ numberOfGlyphs,
+ glyphMetrics );
+
+ float interGlyphAdvance = 0.f;
+ if( !isLastPosition &&
+ ( numberOfCharacters > 1u ) )
+ {
+ const CharacterIndex firstIndex = *( mVisualModel->mGlyphsToCharacters.Begin() + currentGlyphIndex );
+ interGlyphAdvance = static_cast<float>( characterIndex - firstIndex ) * glyphMetrics.advance / static_cast<float>( numberOfCharacters );
+ }
+
+ // Get the glyph position and x bearing.
+ const Vector2& currentPosition = *( mVisualModel->mGlyphPositions.Begin() + currentGlyphIndex );
+
+ // Set the cursor's height.
+ cursorInfo.primaryCursorHeight = glyphMetrics.fontHeight;
+
+ // Set the position.
+ cursorInfo.primaryPosition.x = -glyphMetrics.xBearing + currentPosition.x + ( isCurrentRightToLeft ? glyphMetrics.advance : interGlyphAdvance );
+ cursorInfo.primaryPosition.y = line.ascender - glyphMetrics.ascender;
+
+ if( isLastPosition )
+ {
+ // The position of the cursor after the last character needs special
+ // care depending on its direction and the direction of the paragraph.
+
+ if( cursorInfo.isSecondaryCursor )
+ {
+ // 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.
+
+ // TODO: check for more than one line!
+ characterIndex = isRightToLeftParagraph ? line.characterRun.characterIndex : line.characterRun.characterIndex + line.characterRun.numberOfCharacters - 1u;
+ characterIndex = mLogicalModel->GetLogicalCharacterIndex( characterIndex );
+
+ const GlyphIndex glyphIndex = *( mVisualModel->mCharactersToGlyph.Begin() + characterIndex );
+ const Length numberOfGlyphs = *( mVisualModel->mGlyphsPerCharacter.Begin() + characterIndex );
+
+ const Vector2& position = *( mVisualModel->mGlyphPositions.Begin() + glyphIndex );
+
+ // Get the metrics for the group of glyphs.
+ GlyphMetrics glyphMetrics;
+ GetGlyphsMetrics( glyphIndex,
+ numberOfGlyphs,
+ glyphMetrics );
+
+ cursorInfo.primaryPosition.x = -glyphMetrics.xBearing + position.x + ( isRightToLeftParagraph ? 0.f : glyphMetrics.advance );
+
+ cursorInfo.primaryPosition.y = line.ascender - glyphMetrics.ascender;
+ }
+ else
+ {
+ if( !isCurrentRightToLeft )
+ {
+ cursorInfo.primaryPosition.x += glyphMetrics.advance;
+ }
+ else
+ {
+ cursorInfo.primaryPosition.x -= glyphMetrics.advance;
+ }
+ }
+ }
+
+ // Set the alternative cursor position.
+ if( cursorInfo.isSecondaryCursor )
+ {
+ // Convert the cursor position into the glyph position.
+ const CharacterIndex previousCharacterIndex = ( ( isRightToLeftParagraph != isCurrentRightToLeft ) ? logical : previousLogical );
+ const GlyphIndex previousGlyphIndex = *( mVisualModel->mCharactersToGlyph.Begin() + previousCharacterIndex );
+ const Length numberOfGlyphs = *( mVisualModel->mGlyphsPerCharacter.Begin() + previousCharacterIndex );
+
+ // Get the glyph position.
+ const Vector2& previousPosition = *( mVisualModel->mGlyphPositions.Begin() + previousGlyphIndex );
+
+ // Get the metrics for the group of glyphs.
+ GlyphMetrics glyphMetrics;
+ GetGlyphsMetrics( previousGlyphIndex,
+ numberOfGlyphs,
+ glyphMetrics );
+
+ // Set the cursor position and height.
+ cursorInfo.secondaryPosition.x = -glyphMetrics.xBearing + previousPosition.x + ( ( ( isLastPosition && !isCurrentRightToLeft ) ||
+ ( !isLastPosition && isCurrentRightToLeft ) ) ? glyphMetrics.advance : 0.f );
+
+ cursorInfo.secondaryCursorHeight = 0.5f * glyphMetrics.fontHeight;
+
+ cursorInfo.secondaryPosition.y = cursorInfo.lineHeight - cursorInfo.secondaryCursorHeight - line.descender - ( glyphMetrics.fontHeight - glyphMetrics.ascender );
+
+ // Update the primary cursor height as well.
+ cursorInfo.primaryCursorHeight *= 0.5f;
+ }
+ }
+
+ /**
+ * @brief Get some glyph's metrics of a group of glyphs formed as a result of shaping one character.
+ *
+ * @param[in] glyphIndex The index to the first glyph.
+ * @param[in] numberOfGlyphs The number of glyphs.
+ * @param[out] glyphMetrics Some glyph metrics (font height, advance, ascender and x bearing).
+ */
+ void GetGlyphsMetrics( GlyphIndex glyphIndex,
+ Length numberOfGlyphs,
+ GlyphMetrics& glyphMetrics ) const
+ {
+ const GlyphInfo* glyphsBuffer = mVisualModel->mGlyphs.Begin();
+
+ const GlyphInfo& firstGlyph = *( glyphsBuffer + glyphIndex );
+
+ Text::FontMetrics fontMetrics;
+ mFontClient.GetFontMetrics( firstGlyph.fontId, fontMetrics );
+
+ glyphMetrics.fontHeight = fontMetrics.height;
+ glyphMetrics.advance = firstGlyph.advance;
+ glyphMetrics.ascender = fontMetrics.ascender;
+ glyphMetrics.xBearing = firstGlyph.xBearing;
+
+ for( unsigned int i = 1u; i < numberOfGlyphs; ++i )
+ {
+ const GlyphInfo& glyphInfo = *( glyphsBuffer + glyphIndex + i );
+
+ glyphMetrics.advance += glyphInfo.advance;
+ }
+ }
+
+ /**
+ * @brief Calculates the new cursor index.
+ *
+ * It takes into account that in some scripts multiple characters can form a glyph and all of them
+ * need to be jumped with one key event.
+ *
+ * @param[in] index The initial new index.
+ *
+ * @return The new cursor index.
+ */
+ CharacterIndex CalculateNewCursorIndex( CharacterIndex index ) const
+ {
+ CharacterIndex cursorIndex = mPrimaryCursorPosition;
+
+ const Script script = mLogicalModel->GetScript( index );
+ const GlyphIndex* charactersToGlyphBuffer = mVisualModel->mCharactersToGlyph.Begin();
+ const Length* charactersPerGlyphBuffer = mVisualModel->mCharactersPerGlyph.Begin();
+
+ Length numberOfCharacters = 0u;
+ if( TextAbstraction::LATIN == script )
+ {
+ // Prevents to jump the whole Latin ligatures like fi, ff, ...
+ numberOfCharacters = 1u;
+ }
+ else
+ {
+ GlyphIndex glyphIndex = *( charactersToGlyphBuffer + index );
+ numberOfCharacters = *( charactersPerGlyphBuffer + glyphIndex );
+
+ while( 0u == numberOfCharacters )
+ {
+ numberOfCharacters = *( charactersPerGlyphBuffer + glyphIndex );
+ ++glyphIndex;
+ }
+ }
+
+ if( index < mPrimaryCursorPosition )
+ {
+ cursorIndex -= numberOfCharacters;
+ }
+ else
+ {
+ cursorIndex += numberOfCharacters;
+ }
+
+ return cursorIndex;
+ }
+
+ void UpdateCursorPosition()
+ {
+ CursorInfo cursorInfo;
+
+ GetCursorPosition( mPrimaryCursorPosition,
+ cursorInfo );
+
+ mDecorator->SetPosition( PRIMARY_CURSOR,
+ cursorInfo.primaryPosition.x,
+ cursorInfo.primaryPosition.y,
+ cursorInfo.primaryCursorHeight,
+ cursorInfo.lineHeight );
+
+ if( cursorInfo.isSecondaryCursor )
+ {
+ mDecorator->SetActiveCursor( ACTIVE_CURSOR_BOTH );
+ mDecorator->SetPosition( SECONDARY_CURSOR,
+ cursorInfo.secondaryPosition.x,
+ cursorInfo.secondaryPosition.y,
+ cursorInfo.secondaryCursorHeight,
+ cursorInfo.lineHeight );
+ }
+ else
+ {
+ mDecorator->SetActiveCursor( ACTIVE_CURSOR_PRIMARY );
+ }
+
+ mUpdateCursorPosition = false;
+ mDecoratorUpdated = true;
+ }
+
+ LogicalModelPtr mLogicalModel;
+ VisualModelPtr mVisualModel;
+ DecoratorPtr mDecorator;
+ FontDefaults* mFontDefaults;
+ TextAbstraction::FontClient& mFontClient;
+ std::string mPlaceholderText;
+
+ /**
+ * This is used to delay handling events until after the model has been updated.
+ * The number of updates to the model is minimized to improve performance.
+ */
+ vector<Event> mEventQueue; ///< The queue of touch events etc.
+
+ State mState; ///< Selection mode, edit mode etc.
+
+ CharacterIndex mPrimaryCursorPosition; ///< Index into logical model for primary cursor
+ CharacterIndex mSecondaryCursorPosition; ///< Index into logical model for secondary cursor
+
+ /**
+ * 0,0 means that the top-left corner of the layout matches the top-left corner of the UI control.
+ * Typically this will have a negative value with scrolling occurs.
+ */
+ Vector2 mScrollPosition; ///< The text is offset by this position when scrolling.
+
+ bool mDecoratorUpdated : 1; ///< True if the decorator was updated during event processing
+ bool mCursorBlinkEnabled : 1; ///< True if cursor should blink when active
+ bool mGrabHandleEnabled : 1; ///< True if grab handle is enabled
+ bool mGrabHandlePopupEnabled : 1; ///< True if the grab handle popu-up should be shown
+ bool mSelectionEnabled : 1; ///< True if selection handles, highlight etc. are enabled
+ bool mHorizontalScrollingEnabled : 1; ///< True if horizontal scrolling is enabled
+ bool mVerticalScrollingEnabled : 1; ///< True if vertical scrolling is enabled
+ bool mUpdateCursorPosition : 1; ///< True if the visual position of the cursor must be recalculated
+};
+
+struct Controller::Impl
+{
+ Impl( ControlInterface& controlInterface )
+ : mControlInterface( controlInterface ),
+ mLogicalModel(),
+ mVisualModel(),
+ mFontDefaults( NULL ),
+ mTextInput( NULL ),
+ mFontClient(),
+ mView(),
+ mLayoutEngine(),
+ mModifyEvents(),
+ mControlSize(),
+ mAlignmentOffset(),
+ mOperationsPending( NO_OPERATION ),
+ mRecalculateNaturalSize( true )
+ {
+ mLogicalModel = LogicalModel::New();
+ mVisualModel = VisualModel::New();
+
+ mFontClient = TextAbstraction::FontClient::Get();
+
+ mView.SetVisualModel( mVisualModel );
+
+ // Set the text properties to default
+ mVisualModel->SetTextColor( Color::WHITE );
+ mVisualModel->SetShadowOffset( Vector2::ZERO );
+ mVisualModel->SetShadowColor( Color::BLACK );
+ mVisualModel->SetUnderlineEnabled( false );
+ mVisualModel->SetUnderlineHeight( 0.0f );
+ }
+
+ ~Impl()
+ {
+ delete mTextInput;
+ }
+
+ ControlInterface& mControlInterface; ///< Reference to the text controller.
+ LogicalModelPtr mLogicalModel; ///< Pointer to the logical model.
+ VisualModelPtr mVisualModel; ///< Pointer to the visual model.
+ FontDefaults* mFontDefaults; ///< Avoid allocating this when the user does not specify a font.
+ Controller::TextInput* mTextInput; ///< Avoid allocating everything for text input until EnableTextInput().
+ TextAbstraction::FontClient mFontClient; ///< Handle to the font client.
+ View mView; ///< The view interface to the rendering back-end.
+ LayoutEngine mLayoutEngine; ///< The layout engine.
+ std::vector<ModifyEvent> mModifyEvents; ///< Temporary stores the text set until the next relayout.
+ Size mControlSize; ///< The size of the control.
+ Vector2 mAlignmentOffset; ///< Vertical and horizontal offset of the whole text inside the control due to alignment.
+ OperationsMask mOperationsPending; ///< Operations pending to be done to layout the text.
+ bool mRecalculateNaturalSize:1; ///< Whether the natural size needs to be recalculated.
+};
+
+ControllerPtr Controller::New( ControlInterface& controlInterface )
+{
+ return ControllerPtr( new Controller( controlInterface ) );
+}
+
+void Controller::SetText( const std::string& text )
+{
+ // Cancel previously queued inserts etc.
+ mImpl->mModifyEvents.clear();
+
+ // Keep until size negotiation
+ ModifyEvent event;
+ event.type = REPLACE_TEXT;
+ event.text = text;
+ mImpl->mModifyEvents.push_back( event );
+
+ if( mImpl->mTextInput )
+ {
+ // Cancel previously queued events
+ mImpl->mTextInput->mEventQueue.clear();
+
+ // TODO - Hide selection decorations
+ }
+}
+
+void Controller::GetText( std::string& text ) const
+{
+ if( !mImpl->mModifyEvents.empty() &&
+ REPLACE_TEXT == mImpl->mModifyEvents[0].type )
+ {
+ text = mImpl->mModifyEvents[0].text;
+ }
+ else
+ {
+ // TODO - Convert from UTF-32
+ }
+}
+
+void Controller::SetPlaceholderText( const std::string& text )
+{
+ if( !mImpl->mTextInput )
+ {
+ mImpl->mTextInput->mPlaceholderText = text;
+ }
+}
+
+void Controller::GetPlaceholderText( std::string& text ) const
+{
+ if( !mImpl->mTextInput )
+ {
+ text = mImpl->mTextInput->mPlaceholderText;
+ }
+}
+
+void Controller::SetDefaultFontFamily( const std::string& defaultFontFamily )
+{
+ if( !mImpl->mFontDefaults )
+ {
+ mImpl->mFontDefaults = new Controller::FontDefaults();
+ }
+
+ mImpl->mFontDefaults->mDefaultFontFamily = defaultFontFamily;
+ mImpl->mFontDefaults->mFontId = 0u; // Remove old font ID
+ mImpl->mOperationsPending = ALL_OPERATIONS;
+ mImpl->mRecalculateNaturalSize = true;
+
+ // Clear the font-specific data
+ mImpl->mLogicalModel->mFontRuns.Clear();
+ mImpl->mVisualModel->mGlyphs.Clear();
+ mImpl->mVisualModel->mGlyphsToCharacters.Clear();
+ mImpl->mVisualModel->mCharactersToGlyph.Clear();
+ mImpl->mVisualModel->mCharactersPerGlyph.Clear();
+ mImpl->mVisualModel->mGlyphsPerCharacter.Clear();
+ mImpl->mVisualModel->mGlyphPositions.Clear();
+ mImpl->mVisualModel->mLines.Clear();
+ mImpl->mVisualModel->ClearCaches();
+
+ RequestRelayout();
+}
+
+const std::string& Controller::GetDefaultFontFamily() const
+{
+ if( mImpl->mFontDefaults )
+ {
+ return mImpl->mFontDefaults->mDefaultFontFamily;
+ }
+
+ return EMPTY_STRING;
+}
+
+void Controller::SetDefaultFontStyle( const std::string& defaultFontStyle )
+{
+ if( !mImpl->mFontDefaults )
+ {
+ mImpl->mFontDefaults = new Controller::FontDefaults();
+ }
+
+ mImpl->mFontDefaults->mDefaultFontStyle = defaultFontStyle;
+ mImpl->mFontDefaults->mFontId = 0u; // Remove old font ID
+ mImpl->mOperationsPending = ALL_OPERATIONS;
+ mImpl->mRecalculateNaturalSize = true;
+
+ // Clear the font-specific data
+ mImpl->mLogicalModel->mFontRuns.Clear();
+ mImpl->mVisualModel->mGlyphs.Clear();
+ mImpl->mVisualModel->mGlyphsToCharacters.Clear();
+ mImpl->mVisualModel->mCharactersToGlyph.Clear();
+ mImpl->mVisualModel->mCharactersPerGlyph.Clear();
+ mImpl->mVisualModel->mGlyphsPerCharacter.Clear();
+ mImpl->mVisualModel->mGlyphPositions.Clear();
+ mImpl->mVisualModel->mLines.Clear();
+ mImpl->mVisualModel->ClearCaches();
+
+ RequestRelayout();
+}
+
+const std::string& Controller::GetDefaultFontStyle() const
+{
+ if( mImpl->mFontDefaults )
+ {
+ return mImpl->mFontDefaults->mDefaultFontStyle;
+ }
+
+ return EMPTY_STRING;
+}
+
+void Controller::SetDefaultPointSize( float pointSize )
+{
+ if( !mImpl->mFontDefaults )
+ {
+ mImpl->mFontDefaults = new Controller::FontDefaults();
+ }
+
+ mImpl->mFontDefaults->mDefaultPointSize = pointSize;
+ mImpl->mFontDefaults->mFontId = 0u; // Remove old font ID
+ mImpl->mOperationsPending = ALL_OPERATIONS;
+ mImpl->mRecalculateNaturalSize = true;
+
+ // Clear the font-specific data
+ mImpl->mLogicalModel->mFontRuns.Clear();
+ mImpl->mVisualModel->mGlyphs.Clear();
+ mImpl->mVisualModel->mGlyphsToCharacters.Clear();
+ mImpl->mVisualModel->mCharactersToGlyph.Clear();
+ mImpl->mVisualModel->mCharactersPerGlyph.Clear();
+ mImpl->mVisualModel->mGlyphsPerCharacter.Clear();
+ mImpl->mVisualModel->mGlyphPositions.Clear();
+ mImpl->mVisualModel->mLines.Clear();
+ mImpl->mVisualModel->ClearCaches();
+
+ RequestRelayout();
+}
+
+float Controller::GetDefaultPointSize() const
+{
+ if( mImpl->mFontDefaults )
+ {
+ return mImpl->mFontDefaults->mDefaultPointSize;
+ }
+
+ return 0.0f;
+}
+
+void Controller::GetDefaultFonts( Vector<FontRun>& fonts, Length numberOfCharacters ) const
+{
+ if( mImpl->mFontDefaults )
+ {
+ FontRun fontRun;
+ fontRun.characterRun.characterIndex = 0;
+ fontRun.characterRun.numberOfCharacters = numberOfCharacters;
+ fontRun.fontId = mImpl->mFontDefaults->GetFontId( mImpl->mFontClient );
+ fontRun.isDefault = true;
+
+ fonts.PushBack( fontRun );
+ }
+}
+
+const Vector4& Controller::GetTextColor() const
+{
+ return mImpl->mVisualModel->GetTextColor();
+}
+
+const Vector2& Controller::GetShadowOffset() const
+{
+ return mImpl->mVisualModel->GetShadowOffset();
+}
+
+const Vector4& Controller::GetShadowColor() const
+{
+ return mImpl->mVisualModel->GetShadowColor();
+}
+
+const Vector4& Controller::GetUnderlineColor() const
+{
+ return mImpl->mVisualModel->GetUnderlineColor();
+}
+
+bool Controller::IsUnderlineEnabled() const
+{
+ return mImpl->mVisualModel->IsUnderlineEnabled();
+}
+
+float Controller::GetUnderlineHeight() const
+{
+ return mImpl->mVisualModel->GetUnderlineHeight();
+}
+
+void Controller::SetTextColor( const Vector4& textColor )
+{
+ mImpl->mVisualModel->SetTextColor( textColor );
+}
+
+void Controller::SetShadowOffset( const Vector2& shadowOffset )
+{
+ mImpl->mVisualModel->SetShadowOffset( shadowOffset );
+}
+
+void Controller::SetShadowColor( const Vector4& shadowColor )
+{
+ mImpl->mVisualModel->SetShadowColor( shadowColor );
+}
+
+void Controller::SetUnderlineColor( const Vector4& color )
+{
+ mImpl->mVisualModel->SetUnderlineColor( color );
+}
+
+void Controller::SetUnderlineEnabled( bool enabled )
+{
+ mImpl->mVisualModel->SetUnderlineEnabled( enabled );
+}
+
+void Controller::SetUnderlineHeight( float height )
+{
+ mImpl->mVisualModel->SetUnderlineHeight( height );
+}
+
+void Controller::EnableTextInput( DecoratorPtr decorator )
+{
+ if( !mImpl->mTextInput )
+ {
+ mImpl->mTextInput = new TextInput( mImpl->mLogicalModel,
+ mImpl->mVisualModel,
+ decorator,
+ mImpl->mFontDefaults,
+ mImpl->mFontClient );
+ }
+}
+
+void Controller::SetEnableCursorBlink( bool enable )
+{
+ DALI_ASSERT_DEBUG( NULL != mImpl->mTextInput && "TextInput disabled" );
+
+ if( mImpl->mTextInput )
+ {
+ mImpl->mTextInput->mCursorBlinkEnabled = enable;
+
+ if( !enable &&
+ mImpl->mTextInput->mDecorator )
+ {
+ mImpl->mTextInput->mDecorator->StopCursorBlink();
+ }
+ }
+}
+
+bool Controller::GetEnableCursorBlink() const
+{
+ if( mImpl->mTextInput )
+ {
+ return mImpl->mTextInput->mCursorBlinkEnabled;
+ }
+
+ return false;
+}
+
+const Vector2& Controller::GetScrollPosition() const
+{
+ if( mImpl->mTextInput )
+ {
+ return mImpl->mTextInput->mScrollPosition;
+ }
+
+ return Vector2::ZERO;
+}
+
+const Vector2& Controller::GetAlignmentOffset() const
+{
+ return mImpl->mAlignmentOffset;
+}
+
+Vector3 Controller::GetNaturalSize()
+{
+ Vector3 naturalSize;
+
+ // Make sure the model is up-to-date before layouting
+ ProcessModifyEvents();