Handle Emoji clustering for cursor handling 32/272432/9
authorssabah <s.sabah@samsung.com>
Wed, 16 Mar 2022 14:32:21 +0000 (17:32 +0300)
committershrouq Sabah <s.sabah@samsung.com>
Sun, 27 Mar 2022 08:50:27 +0000 (08:50 +0000)
Checked cases:
  - cursor movement (arrows): it works fine
  - line-breaking: it works fine
Resolved cases:
  - deletion: this patch handled delete cases when the cursor is before or after Emoji
    :: Before: when use delete key and cursor is before Emoji
    :: After: when use backspace key and cursor is after Emoji
  - Emoji layoutted at the end of line: this patch handle the below scenario
    1) When there is Emoji contains multi unicodes and it is layoutted at the end of line (LineWrap case , is not new line case)
    2) Try to click at the center or at the end of Emoji then the cursor appears inside Emoji
    3) Example:"FamilyManWomanGirlBoy &#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;"

 Sample code to test:
 ============================================================================
    TextEditor textEditor = TextEditor::New();
    textEditor.SetBackgroundColor(Dali::Color::GRAY);
    textEditor.SetProperty(Dali::Toolkit::TextEditor::Property::TEXT, "AB&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;AB&#x1F469;&#x1F3FB;&#x200D;&#x1F52C;B&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;AA&#x262a;&#xfe0e;B&#x262a;&#xfe0f;AB");
    textEditor.SetProperty(Actor::Property::ANCHOR_POINT, AnchorPoint::TOP_LEFT);
    textEditor.SetProperty(Dali::Actor::Property::POSITION, Vector2(10, 30));
    textEditor.SetProperty(Dali::Actor::Property::SIZE, Vector2(100, 100));
    textEditor.SetProperty(Dali::Toolkit::TextEditor::Property::ENABLE_MARKUP, true);
    textEditor.SetProperty(Dali::Toolkit::DevelTextEditor::Property::LINE_WRAP_MODE, Dali::Toolkit::DevelText::LineWrap::WORD);

 ============================================================================

Change-Id: I6d142c58c5cdef5e900404cca8b777a02c8488d1

automated-tests/src/dali-toolkit-internal/CMakeLists.txt
automated-tests/src/dali-toolkit-internal/utc-Dali-Text-Characters.cpp [new file with mode: 0755]
automated-tests/src/dali-toolkit-internal/utc-Dali-Text-Cursor.cpp
automated-tests/src/dali-toolkit/utc-Dali-TextEditor.cpp
automated-tests/src/dali-toolkit/utc-Dali-TextField.cpp
dali-toolkit/internal/file.list
dali-toolkit/internal/text/characters-helper-functions.cpp [new file with mode: 0644]
dali-toolkit/internal/text/characters-helper-functions.h [new file with mode: 0644]
dali-toolkit/internal/text/cursor-helper-functions.cpp
dali-toolkit/internal/text/text-controller-text-updater.cpp

index 198d183..5ba86de 100755 (executable)
@@ -18,6 +18,7 @@ SET(TC_SOURCES
  utc-Dali-ItemView-internal.cpp
  utc-Dali-LogicalModel.cpp
  utc-Dali-PropertyHelper.cpp
+ utc-Dali-Text-Characters.cpp
  utc-Dali-Text-CharacterSetConversion.cpp
  utc-Dali-Text-Circular.cpp
  utc-Dali-Text-Controller.cpp
diff --git a/automated-tests/src/dali-toolkit-internal/utc-Dali-Text-Characters.cpp b/automated-tests/src/dali-toolkit-internal/utc-Dali-Text-Characters.cpp
new file mode 100755 (executable)
index 0000000..259d682
--- /dev/null
@@ -0,0 +1,189 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <iostream>
+
+#include <dali-toolkit-test-suite-utils.h>
+#include <dali-toolkit/dali-toolkit.h>
+#include <dali-toolkit/internal/text/characters-helper-functions.h>
+#include <toolkit-text-utils.h>
+
+using namespace Dali;
+using namespace Toolkit;
+using namespace Text;
+
+// Tests the following function.
+//
+//  CharacterRun RetrieveClusteredCharactersOfCharacterIndex(const VisualModelPtr&  visualModel,
+//                                                          const LogicalModelPtr& logicalModel,
+//                                                          const CharacterIndex&  characterIndex)
+
+//////////////////////////////////////////////////////////
+
+namespace
+{
+const std::string  DEFAULT_FONT_DIR("/resources/fonts");
+const unsigned int DEFAULT_FONT_SIZE = 1152u;
+
+struct RetrieveClusteredCharactersOfCharacterIndexData
+{
+  std::string     description;            ///< Description of the test.
+  std::string     text;                   ///< Input text.
+  bool            markupProcessorEnabled; //< Enable markup processor to use markup text.
+  unsigned int    numberOfTests;          ///< The number of tests.
+  CharacterIndex* characterIndex;         ///< The character index for each test.
+  CharacterRun*   clusteredCharacters;    ///< The expected clustered characters run for each test.
+};
+
+bool GetRetrieveClusteredCharactersOfCharacterIndexTest(const RetrieveClusteredCharactersOfCharacterIndexData& data)
+{
+  std::cout << "  testing : " << data.description << std::endl;
+
+  // 1) Create the model.
+  ModelPtr   textModel;
+  MetricsPtr metrics;
+  Size       textArea(400.f, 600.f);
+  Size       layoutSize;
+
+  Vector<FontDescriptionRun> fontDescriptionRuns;
+  LayoutOptions              options;
+  CreateTextModel(data.text,
+                  textArea,
+                  fontDescriptionRuns,
+                  options,
+                  layoutSize,
+                  textModel,
+                  metrics,
+                  data.markupProcessorEnabled,
+                  LineWrap::WORD,
+                  false,
+                  Toolkit::DevelText::EllipsisPosition::END,
+                  0.0f, // lineSpacing
+                  0.0f  // characterSpacing
+  );
+
+  LogicalModelPtr logicalModel = textModel->mLogicalModel;
+  VisualModelPtr  visualModel  = textModel->mVisualModel;
+
+  for(unsigned int index = 0; index < data.numberOfTests; ++index)
+  {
+    CharacterRun clusteredCharacters = RetrieveClusteredCharactersOfCharacterIndex(visualModel, logicalModel, data.characterIndex[index]);
+
+    if(clusteredCharacters.characterIndex != data.clusteredCharacters[index].characterIndex)
+    {
+      std::cout << "  test " << index << " failed. Different clusteredCharacters.characterIndex : " << clusteredCharacters.characterIndex << ", expected : " << data.clusteredCharacters[index].characterIndex << std::endl;
+      return false;
+    }
+
+    if(clusteredCharacters.numberOfCharacters != data.clusteredCharacters[index].numberOfCharacters)
+    {
+      std::cout << "  test " << index << " failed. Different clusteredCharacters.numberOfCharacters : " << clusteredCharacters.numberOfCharacters << ", expected : " << data.clusteredCharacters[index].numberOfCharacters << std::endl;
+      return false;
+    }
+  }
+
+  return true;
+}
+
+} // namespace
+
+//////////////////////////////////////////////////////////
+//
+// UtcDaliRetrieveClusteredCharactersOfCharacterIndex
+//
+//////////////////////////////////////////////////////////
+
+int UtcDaliRetrieveClusteredCharactersOfCharacterIndex(void)
+{
+  tet_infoline(" UtcDaliRetrieveClusteredCharactersOfCharacterIndex");
+
+  CharacterIndex characterIndex01[] = {0u, 1u, 2u, 10u};
+
+  CharacterRun clusteredCharacters01[] = {
+    {0u, 1u},
+    {1u, 1u},
+    {2u, 1u},
+    {10u, 1u}};
+
+  CharacterIndex characterIndex02[] = {0u, 4u, 6u};
+
+  CharacterRun clusteredCharacters02[] = {
+    {0u, 7u},
+    {0u, 7u},
+    {0u, 7u}};
+
+  CharacterIndex characterIndex03[] = {3u, 9u, 14u};
+
+  CharacterRun clusteredCharacters03[] = {
+    {2u, 7u},
+    {9u, 1u},
+    {11u, 4u}};
+
+  CharacterIndex characterIndex04[] = {0u, 1u, 2u, 10u};
+
+  CharacterRun clusteredCharacters04[] = {
+    {0u, 1u},
+    {1u, 1u},
+    {2u, 1u},
+    {10u, 1u}};
+
+  struct RetrieveClusteredCharactersOfCharacterIndexData data[] =
+    {
+      {"Easy latin script",
+       "Hello world",
+       true,
+       4u,
+       characterIndex01,
+       clusteredCharacters01},
+
+      {"FamilyManWomanGirlBoy Single Complex Emoji script",
+       "&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;",
+       true,
+       3u,
+       characterIndex02,
+       clusteredCharacters02},
+
+      {"Long text many Emojis with letters",
+       "AB&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;AB&#x1F469;&#x1F3FB;&#x200D;&#x1F52C;B&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;AA&#x262a;&#xfe0e;B&#x262a;&#xfe0f;AB",
+       true,
+       3u,
+       characterIndex03,
+       clusteredCharacters03},
+
+      {"Arabic script",
+       "اهلا و سهلا",
+       true,
+       4u,
+       characterIndex04,
+       clusteredCharacters04},
+    };
+  const unsigned int numberOfTests = 4u;
+
+  for(unsigned int index = 0; index < numberOfTests; ++index)
+  {
+    ToolkitTestApplication application;
+    if(!GetRetrieveClusteredCharactersOfCharacterIndexTest(data[index]))
+    {
+      tet_result(TET_FAIL);
+    }
+  }
+
+  tet_result(TET_PASS);
+  END_TEST;
+}
index 11c6343..cd1abcb 100755 (executable)
@@ -60,14 +60,15 @@ struct GetClosestLineData
 
 struct GetClosestCursorIndexData
 {
-  std::string             description;    ///< Description of the test.
-  std::string             text;           ///< Input text.
-  unsigned int            numberOfTests;  ///< The number of tests.
-  float*                  visualX;        ///< The visual 'x' position for each test.
-  float*                  visualY;        ///< The visual 'y' position for each test.
-  CharacterHitTest::Mode* mode;           ///< The type of hit test.
-  CharacterIndex*         logicalIndex;   ///< The expected logical cursor index for each test.
-  bool*                   isCharacterHit; ///< The expected character hit value for each test.
+  std::string             description;            ///< Description of the test.
+  std::string             text;                   ///< Input text.
+  unsigned int            numberOfTests;          ///< The number of tests.
+  float*                  visualX;                ///< The visual 'x' position for each test.
+  float*                  visualY;                ///< The visual 'y' position for each test.
+  CharacterHitTest::Mode* mode;                   ///< The type of hit test.
+  bool                    markupProcessorEnabled; //< Enable markup processor to use markup text.
+  CharacterIndex*         logicalIndex;           ///< The expected logical cursor index for each test.
+  bool*                   isCharacterHit;         ///< The expected character hit value for each test.
 };
 
 struct GetCursorPositionData
@@ -173,7 +174,7 @@ bool GetClosestCursorIndexTest(const GetClosestCursorIndexData& data)
                   layoutSize,
                   textModel,
                   metrics,
-                  false,
+                  data.markupProcessorEnabled,
                   LineWrap::WORD,
                   false,
                   Toolkit::DevelText::EllipsisPosition::END,
@@ -579,6 +580,12 @@ int UtcDaliGetClosestCursorIndex(void)
   CharacterIndex         logicalIndex08[]   = {1u};
   bool                   isCharacterHit08[] = {true};
 
+  float                  visualX09[]        = {9.f};
+  float                  visualY09[]        = {12.f};
+  CharacterHitTest::Mode mode09[]           = {CharacterHitTest::TAP};
+  CharacterIndex         logicalIndex09[]   = {1u};
+  bool                   isCharacterHit09[] = {true};
+
   struct GetClosestCursorIndexData data[] =
     {
       {"Void text.",
@@ -587,24 +594,30 @@ int UtcDaliGetClosestCursorIndex(void)
        visualX01,
        visualY01,
        mode01,
+       false,
        logicalIndex01,
        isCharacterHit01},
+
       {"Single line text.",
        "Hello world שלום עולם",
        7u,
        visualX02,
        visualY02,
        mode02,
+       false,
        logicalIndex02,
        isCharacterHit02},
+
       {"Single line with ligatures",
        "different الأربعاء",
        4u,
        visualX03,
        visualY03,
        mode03,
+       false,
        logicalIndex03,
        isCharacterHit03},
+
       {"Multiline. Single line paragraphs",
        "Hello world\n"
        "שלום עולם\n"
@@ -613,8 +626,10 @@ int UtcDaliGetClosestCursorIndex(void)
        visualX04,
        visualY04,
        mode04,
+       false,
        logicalIndex04,
        isCharacterHit04},
+
       {"Multiline. Single bidirectional paragraph, starts LTR, wrapped lines",
        "abcשנבdefגקכghiעיןjklחלךmnoצמםpqrפרףstuדאוvwxה"
        "סתyzטזץabcשנבdefגקכghiעיןjklחלךmnoצמםpqrפרףstuד"
@@ -626,8 +641,10 @@ int UtcDaliGetClosestCursorIndex(void)
        visualX05,
        visualY05,
        mode05,
+       false,
        logicalIndex05,
        isCharacterHit05},
+
       {"Multiline. Single bidirectional paragraph, starts RTL, wrapped lines",
        "שנבabcגקכdefעיןghiחלךjklצמםmnoפרףpqrדאוstuהסתv"
        "wxטזץyzשנבabcגקכdefעיןghiחלךjklצמםmnoפרףpqrדאוs"
@@ -639,25 +656,42 @@ int UtcDaliGetClosestCursorIndex(void)
        visualX06,
        visualY06,
        mode06,
+       false,
        logicalIndex06,
        isCharacterHit06},
+
       {"Testing complex characters. Arabic ligatures",
        "الأَبْجَدِيَّة العَرَبِيَّة",
        1u,
        visualX07,
        visualY07,
        mode07,
+       false,
        logicalIndex07,
        isCharacterHit07},
+
       {"Testing complex characters. Latin ligatures",
        "fi ligature",
        1u,
        visualX08,
        visualY08,
        mode08,
+       false,
        logicalIndex08,
-       isCharacterHit08}};
-  const unsigned int numberOfTests = 8u;
+       isCharacterHit08},
+
+      {"Testing complex characters. Emoji",
+       "A&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;B",
+       1u,
+       visualX09,
+       visualY09,
+       mode09,
+       true,
+       logicalIndex09,
+       isCharacterHit09}
+
+    };
+  const unsigned int numberOfTests = 9u;
 
   for(unsigned int index = 0; index < numberOfTests; ++index)
   {
index 9d9567b..19c3583 100644 (file)
@@ -5924,4 +5924,83 @@ int UtcDaliToolkitTexteditorParagraphTag(void)
   application.Render();
 
   END_TEST;
+}
+
+//Handle Emoji clustering for cursor handling
+int utcDaliTextEditorClusteredEmojiDeletionBackSpaceKey(void)
+{
+  ToolkitTestApplication application;
+  tet_infoline(" utcDaliTextEditorClusteredEmojiDeletionBackSpaceKey ");
+  TextEditor textEditor = TextEditor::New();
+  DALI_TEST_CHECK(textEditor);
+
+  application.GetScene().Add(textEditor);
+
+  // Avoid a crash when core load gl resources.
+  application.GetGlAbstraction().SetCheckFramebufferStatusResult(GL_FRAMEBUFFER_COMPLETE);
+
+  textEditor.SetProperty(TextEditor::Property::TEXT, "ABC&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;XY");
+  textEditor.SetProperty(Dali::Toolkit::TextEditor::Property::ENABLE_MARKUP, true);
+
+  // Render and notify
+  application.SendNotification();
+  application.Render();
+
+  // Set currsor
+  textEditor.SetProperty(DevelTextEditor::Property::PRIMARY_CURSOR_POSITION, 10);
+  application.SendNotification();
+  application.Render();
+
+  // Set focus and remove Emoji
+  textEditor.SetKeyInputFocus();
+  application.ProcessEvent(GenerateKey("", "", "", DALI_KEY_BACKSPACE, 0, 0, Integration::KeyEvent::DOWN, "", DEFAULT_DEVICE_NAME, Device::Class::NONE, Device::Subclass::NONE));
+
+  //Check the changed text and cursor position
+  DALI_TEST_EQUALS(textEditor.GetProperty(TextEditor::Property::TEXT).Get<std::string>(), "ABCXY", TEST_LOCATION);
+  DALI_TEST_EQUALS(textEditor.GetProperty(DevelTextEditor::Property::PRIMARY_CURSOR_POSITION).Get<int>(), 3, TEST_LOCATION);
+
+  // Render and notify
+  application.SendNotification();
+  application.Render();
+
+  END_TEST;
+}
+
+int utcDaliTextEditorClusteredEmojiDeletionDeleteKey(void)
+{
+  ToolkitTestApplication application;
+  tet_infoline(" utcDaliTextEditorClusteredEmojiDeletionDeleteKey ");
+  TextEditor textEditor = TextEditor::New();
+  DALI_TEST_CHECK(textEditor);
+
+  application.GetScene().Add(textEditor);
+
+  // Avoid a crash when core load gl resources.
+  application.GetGlAbstraction().SetCheckFramebufferStatusResult(GL_FRAMEBUFFER_COMPLETE);
+
+  textEditor.SetProperty(TextEditor::Property::TEXT, "ABC&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;XY");
+  textEditor.SetProperty(Dali::Toolkit::TextEditor::Property::ENABLE_MARKUP, true);
+
+  // Render and notify
+  application.SendNotification();
+  application.Render();
+
+  // Set currsor
+  textEditor.SetProperty(DevelTextEditor::Property::PRIMARY_CURSOR_POSITION, 3);
+  application.SendNotification();
+  application.Render();
+
+  // Set focus and remove Emoji
+  textEditor.SetKeyInputFocus();
+  application.ProcessEvent(GenerateKey("", "", "", Dali::DevelKey::DALI_KEY_DELETE, 0, 0, Integration::KeyEvent::DOWN, "", DEFAULT_DEVICE_NAME, Device::Class::NONE, Device::Subclass::NONE));
+
+  //Check the changed text and cursor position
+  DALI_TEST_EQUALS(textEditor.GetProperty(TextEditor::Property::TEXT).Get<std::string>(), "ABCXY", TEST_LOCATION);
+  DALI_TEST_EQUALS(textEditor.GetProperty(DevelTextEditor::Property::PRIMARY_CURSOR_POSITION).Get<int>(), 3, TEST_LOCATION);
+
+  // Render and notify
+  application.SendNotification();
+  application.Render();
+
+  END_TEST;
 }
\ No newline at end of file
index 3f5ff1b..48c2bcc 100644 (file)
@@ -5319,4 +5319,83 @@ int UtcDaliToolkitTextfieldParagraphTag(void)
   application.Render();
 
   END_TEST;
+}
+
+//Handle Emoji clustering for cursor handling
+int utcDaliTextFieldClusteredEmojiDeletionBackSpaceKey(void)
+{
+  ToolkitTestApplication application;
+  tet_infoline(" utcDaliTextFieldClusteredEmojiDeletionBackSpaceKey ");
+  TextField textField = TextField::New();
+  DALI_TEST_CHECK(textField);
+
+  application.GetScene().Add(textField);
+
+  // Avoid a crash when core load gl resources.
+  application.GetGlAbstraction().SetCheckFramebufferStatusResult(GL_FRAMEBUFFER_COMPLETE);
+
+  textField.SetProperty(TextField::Property::TEXT, "ABC&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;XY");
+  textField.SetProperty(Dali::Toolkit::TextField::Property::ENABLE_MARKUP, true);
+
+  // Render and notify
+  application.SendNotification();
+  application.Render();
+
+  // Set currsor
+  textField.SetProperty(DevelTextField::Property::PRIMARY_CURSOR_POSITION, 10);
+  application.SendNotification();
+  application.Render();
+
+  // Set focus and remove Emoji
+  textField.SetKeyInputFocus();
+  application.ProcessEvent(GenerateKey("", "", "", DALI_KEY_BACKSPACE, 0, 0, Integration::KeyEvent::DOWN, "", DEFAULT_DEVICE_NAME, Device::Class::NONE, Device::Subclass::NONE));
+
+  //Check the changed text and cursor position
+  DALI_TEST_EQUALS(textField.GetProperty(TextField::Property::TEXT).Get<std::string>(), "ABCXY", TEST_LOCATION);
+  DALI_TEST_EQUALS(textField.GetProperty(DevelTextField::Property::PRIMARY_CURSOR_POSITION).Get<int>(), 3, TEST_LOCATION);
+
+  // Render and notify
+  application.SendNotification();
+  application.Render();
+
+  END_TEST;
+}
+
+int utcDaliTextFieldClusteredEmojiDeletionDeleteKey(void)
+{
+  ToolkitTestApplication application;
+  tet_infoline(" utcDaliTextFieldClusteredEmojiDeletionDeleteKey ");
+  TextField textField = TextField::New();
+  DALI_TEST_CHECK(textField);
+
+  application.GetScene().Add(textField);
+
+  // Avoid a crash when core load gl resources.
+  application.GetGlAbstraction().SetCheckFramebufferStatusResult(GL_FRAMEBUFFER_COMPLETE);
+
+  textField.SetProperty(TextField::Property::TEXT, "ABC&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;XY");
+  textField.SetProperty(Dali::Toolkit::TextField::Property::ENABLE_MARKUP, true);
+
+  // Render and notify
+  application.SendNotification();
+  application.Render();
+
+  // Set currsor
+  textField.SetProperty(DevelTextField::Property::PRIMARY_CURSOR_POSITION, 3);
+  application.SendNotification();
+  application.Render();
+
+  // Set focus and remove Emoji
+  textField.SetKeyInputFocus();
+  application.ProcessEvent(GenerateKey("", "", "", Dali::DevelKey::DALI_KEY_DELETE, 0, 0, Integration::KeyEvent::DOWN, "", DEFAULT_DEVICE_NAME, Device::Class::NONE, Device::Subclass::NONE));
+
+  //Check the changed text and cursor position
+  DALI_TEST_EQUALS(textField.GetProperty(TextField::Property::TEXT).Get<std::string>(), "ABCXY", TEST_LOCATION);
+  DALI_TEST_EQUALS(textField.GetProperty(DevelTextField::Property::PRIMARY_CURSOR_POSITION).Get<int>(), 3, TEST_LOCATION);
+
+  // Render and notify
+  application.SendNotification();
+  application.Render();
+
+  END_TEST;
 }
\ No newline at end of file
index 7e9c1aa..3fae741 100644 (file)
@@ -140,6 +140,7 @@ SET( toolkit_src_files
    ${toolkit_src_dir}/text/bidirectional-support.cpp
    ${toolkit_src_dir}/text/bounded-paragraph-helper-functions.cpp
    ${toolkit_src_dir}/text/character-set-conversion.cpp
+   ${toolkit_src_dir}/text/characters-helper-functions.cpp
    ${toolkit_src_dir}/text/color-segmentation.cpp
    ${toolkit_src_dir}/text/cursor-helper-functions.cpp
    ${toolkit_src_dir}/text/glyph-metrics-helper.cpp
diff --git a/dali-toolkit/internal/text/characters-helper-functions.cpp b/dali-toolkit/internal/text/characters-helper-functions.cpp
new file mode 100644 (file)
index 0000000..d89f1fb
--- /dev/null
@@ -0,0 +1,60 @@
+// FILE HEADER
+#include <dali-toolkit/internal/text/characters-helper-functions.h>
+
+// INTERNAL INCLUDES
+#include <dali-toolkit/internal/text/emoji-helper.h>
+
+namespace Dali
+{
+namespace Toolkit
+{
+namespace Text
+{
+CharacterRun RetrieveClusteredCharactersOfCharacterIndex(const VisualModelPtr&  visualModel,
+                                                         const LogicalModelPtr& logicalModel,
+                                                         const CharacterIndex&  characterIndex)
+{
+  // Initialization
+  CharacterRun clusteredCharacters;
+  clusteredCharacters.characterIndex     = characterIndex;
+  clusteredCharacters.numberOfCharacters = 1u;
+
+  const GlyphIndex* const     charactersToGlyphBuffer  = visualModel->mCharactersToGlyph.Begin();
+  const Length* const         charactersPerGlyphBuffer = visualModel->mCharactersPerGlyph.Begin();
+  const CharacterIndex* const glyphsToCharacters       = visualModel->mGlyphsToCharacters.Begin();
+
+  GlyphIndex glyphIndex               = *(charactersToGlyphBuffer + characterIndex);
+  Length     actualNumberOfCharacters = *(charactersPerGlyphBuffer + glyphIndex);
+
+  if(actualNumberOfCharacters > 1u)
+  {
+    const Script script = logicalModel->GetScript(characterIndex);
+    // Prevents to break the Latin ligatures like fi, ff, or Arabic ﻻ, ...
+    // Keep actual index of character as is. Because these characters cannot be clustered.
+
+    if(!HasLigatureMustBreak(script))
+    {
+      clusteredCharacters.numberOfCharacters = actualNumberOfCharacters;
+      clusteredCharacters.characterIndex     = *(glyphsToCharacters + glyphIndex); // firstCharacterIndex
+    }
+  }
+  else
+  {
+    while(0u == actualNumberOfCharacters)
+    {
+      ++glyphIndex;
+      actualNumberOfCharacters = *(charactersPerGlyphBuffer + glyphIndex);
+    }
+
+    clusteredCharacters.characterIndex     = *(glyphsToCharacters + glyphIndex); // firstCharacterIndex
+    clusteredCharacters.numberOfCharacters = actualNumberOfCharacters;
+  }
+
+  return clusteredCharacters;
+}
+
+} // namespace Text
+
+} // namespace Toolkit
+
+} // namespace Dali
diff --git a/dali-toolkit/internal/text/characters-helper-functions.h b/dali-toolkit/internal/text/characters-helper-functions.h
new file mode 100644 (file)
index 0000000..00b1b4e
--- /dev/null
@@ -0,0 +1,52 @@
+#ifndef DALI_TOOLKIT_TEXT_CHARACTERS_HELPER_FUNCTIONS_H
+#define DALI_TOOLKIT_TEXT_CHARACTERS_HELPER_FUNCTIONS_H
+
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+// INTERNAL INCLUDES
+#include <dali-toolkit/internal/text/character-run.h>
+#include <dali-toolkit/internal/text/text-definitions.h>
+#include <dali-toolkit/internal/text/text-model.h>
+
+namespace Dali
+{
+namespace Toolkit
+{
+namespace Text
+{
+/**
+ * @brief Retrieve the clustered consecutive characters that contains current character index.
+ * Found the first index of clustered characters and number of characters.
+ *
+ * @param[in] visualModel The visual model.
+ * @param[in] logicalModel The logical model.
+ * @param[in] characterIndex The character index.
+ *
+ * @return CharacterRun for the clustered characters contains character Index
+ */
+CharacterRun RetrieveClusteredCharactersOfCharacterIndex(const VisualModelPtr&  visualModel,
+                                                         const LogicalModelPtr& logicalModel,
+                                                         const CharacterIndex&  characterIndex);
+
+} // namespace Text
+
+} // namespace Toolkit
+
+} // namespace Dali
+
+#endif // DALI_TOOLKIT_TEXT_CHARACTERS_HELPER_FUNCTIONS_H
\ No newline at end of file
index 3bfc059..d895418 100644 (file)
@@ -22,6 +22,8 @@
 #include <dali/integration-api/debug.h>
 
 // INTERNAL INCLUDES
+#include <dali-toolkit/internal/text/characters-helper-functions.h>
+#include <dali-toolkit/internal/text/emoji-helper.h>
 #include <dali-toolkit/internal/text/glyph-metrics-helper.h>
 
 namespace
@@ -458,6 +460,19 @@ CharacterIndex GetClosestCursorIndex(VisualModelPtr         visualModel,
 
   logicalIndex = (bidiLineFetched ? logicalModel->GetLogicalCursorIndex(visualIndex) : visualIndex);
 
+  // Handle Emoji clustering for cursor handling:
+  // Fixing this case:
+  // - When there is Emoji contains multi unicodes and it is layoutted at the end of line (LineWrap case , is not new line case)
+  // - Try to click at the center or at the end of Emoji then the cursor appears inside Emoji
+  // - Example:"FamilyManWomanGirlBoy &#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F467;&#x200D;&#x1F466;"
+  const Script script = logicalModel->GetScript(logicalIndex);
+  if(IsOneOfEmojiScripts(script))
+  {
+    //TODO: Use this clustering for Emoji cases only. This needs more testing to generalize to all scripts.
+    CharacterRun emojiClusteredCharacters = RetrieveClusteredCharactersOfCharacterIndex(visualModel, logicalModel, logicalIndex);
+    logicalIndex                          = emojiClusteredCharacters.characterIndex;
+  }
+
   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");
index 0b24bc0..f49e953 100644 (file)
@@ -24,6 +24,8 @@
 
 // INTERNAL INCLUDES
 #include <dali-toolkit/internal/text/character-set-conversion.h>
+#include <dali-toolkit/internal/text/characters-helper-functions.h>
+#include <dali-toolkit/internal/text/emoji-helper.h>
 #include <dali-toolkit/internal/text/markup-processor.h>
 #include <dali-toolkit/internal/text/text-controller-impl.h>
 #include <dali-toolkit/internal/text/text-controller-placeholder-handler.h>
@@ -481,6 +483,7 @@ bool Controller::TextUpdater::RemoveText(
 
   ModelPtr&        model        = impl.mModel;
   LogicalModelPtr& logicalModel = model->mLogicalModel;
+  VisualModelPtr&  visualModel  = model->mVisualModel;
 
   DALI_LOG_INFO(gLogFilter, Debug::General, "Controller::RemoveText %p mText.Count() %d cursor %d cursorOffset %d numberOfCharacters %d\n", &controller, logicalModel->mText.Count(), eventData->mPrimaryCursorPosition, cursorOffset, numberOfCharacters);
 
@@ -498,6 +501,25 @@ bool Controller::TextUpdater::RemoveText(
       cursorIndex = eventData->mPrimaryCursorPosition + cursorOffset;
     }
 
+    //Handle Emoji clustering for cursor handling
+    // Deletion case: this is handling the deletion cases when the cursor is before or after Emoji
+    //  - Before: when use delete key and cursor is before Emoji (cursorOffset = -1)
+    //  - After: when use backspace key and cursor is after Emoji (cursorOffset = 0)
+
+    const Script script = logicalModel->GetScript(cursorIndex);
+    if((numberOfCharacters == 1u) &&
+       (IsOneOfEmojiScripts(script)))
+    {
+      //TODO: Use this clustering for Emoji cases only. This needs more testing to generalize to all scripts.
+      CharacterRun emojiClusteredCharacters = RetrieveClusteredCharactersOfCharacterIndex(visualModel, logicalModel, cursorIndex);
+      Length       actualNumberOfCharacters = emojiClusteredCharacters.numberOfCharacters;
+
+      //Set cursorIndex at the first characterIndex of clustred Emoji
+      cursorIndex = emojiClusteredCharacters.characterIndex;
+
+      numberOfCharacters = actualNumberOfCharacters;
+    }
+
     if((cursorIndex + numberOfCharacters) > currentText.Count())
     {
       numberOfCharacters = currentText.Count() - cursorIndex;