diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index e9378b2dc3..e192259af4 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -2411,6 +2411,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_gui_basics/widgets/juce_TableListBox.h" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.h" + "../../../../../modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.h" "../../../../../modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" @@ -5016,6 +5017,7 @@ set_source_files_properties( "../../../../../modules/juce_gui_basics/widgets/juce_TableListBox.h" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.h" + "../../../../../modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.h" "../../../../../modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj index 929faf373b..2b8d77ee83 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj @@ -2949,6 +2949,9 @@ true + + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters index db5fed3a0c..9aa3b8c567 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters @@ -3772,6 +3772,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj index 7fe4d50a26..d015877ddc 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj @@ -2949,6 +2949,9 @@ true + + true + true diff --git a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters index 1962ea3086..190dfdcba7 100644 --- a/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2022/DemoRunner_App.vcxproj.filters @@ -3772,6 +3772,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt index b5215a84f9..3d33d355d6 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt @@ -2173,6 +2173,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_gui_basics/widgets/juce_TableListBox.h" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.h" + "../../../../../modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.h" "../../../../../modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" @@ -4438,6 +4439,7 @@ set_source_files_properties( "../../../../../modules/juce_gui_basics/widgets/juce_TableListBox.h" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.h" + "../../../../../modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.h" "../../../../../modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj index 745d7542a7..64aedd0f74 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj @@ -2642,6 +2642,9 @@ true + + true + true diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters index 53982d9232..663f7cc20f 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2022/AudioPerformanceTest_App.vcxproj.filters @@ -3289,6 +3289,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt index b8ba9f0141..218973ad3f 100644 --- a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt @@ -2303,6 +2303,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_gui_basics/widgets/juce_TableListBox.h" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.h" + "../../../../../modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.h" "../../../../../modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" @@ -4721,6 +4722,7 @@ set_source_files_properties( "../../../../../modules/juce_gui_basics/widgets/juce_TableListBox.h" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.h" + "../../../../../modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.h" "../../../../../modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj index e5238541aa..0cf8800307 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj @@ -2776,6 +2776,9 @@ true + + true + true diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters index fbbf3e3027..8d5265970e 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters @@ -3496,6 +3496,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj index 036879efa0..0fc0e20cda 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj @@ -2776,6 +2776,9 @@ true + + true + true diff --git a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters index d790f141bd..4d5a052a12 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2022/AudioPluginHost_App.vcxproj.filters @@ -3496,6 +3496,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt index f5d1a8bacf..daed4e36f3 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt @@ -2192,6 +2192,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_gui_basics/widgets/juce_TableListBox.h" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.h" + "../../../../../modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.h" "../../../../../modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" @@ -4537,6 +4538,7 @@ set_source_files_properties( "../../../../../modules/juce_gui_basics/widgets/juce_TableListBox.h" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_TextEditor.h" + "../../../../../modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.cpp" "../../../../../modules/juce_gui_basics/widgets/juce_Toolbar.h" "../../../../../modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp" diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj index c83ca044e7..d935500995 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj @@ -2663,6 +2663,9 @@ true + + true + true diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters index 2910982e40..db995759f9 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2022/NetworkGraphicsDemo_App.vcxproj.filters @@ -3343,6 +3343,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj index 390cddebdb..78d5648d00 100644 --- a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj @@ -1805,6 +1805,9 @@ true + + true + true diff --git a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj.filters b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj.filters index bc2fbf5b30..b945c7a562 100644 --- a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj.filters +++ b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj.filters @@ -2182,6 +2182,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj index 70dedfbda1..cb88216eb2 100644 --- a/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj @@ -1805,6 +1805,9 @@ true + + true + true diff --git a/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj.filters b/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj.filters index 01215fc723..f02d009a38 100644 --- a/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj.filters +++ b/extras/Projucer/Builds/VisualStudio2022/Projucer_App.vcxproj.filters @@ -2182,6 +2182,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj index f3fd43007e..5214fc06c3 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj @@ -2784,6 +2784,9 @@ true + + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters index 0e5bfd87bb..a711cd88d6 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -3544,6 +3544,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj index ca60af9bad..816716729f 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj @@ -2784,6 +2784,9 @@ true + + true + true diff --git a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters index e58172465c..c25d3f3491 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2022/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -3544,6 +3544,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj index ad0f8c1ffb..7828e62d29 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj @@ -2662,6 +2662,9 @@ true + + true + true diff --git a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters index a45cd5f598..8472645122 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters +++ b/extras/WindowsDLL/Builds/VisualStudio2022/WindowsDLL_DynamicLibrary.vcxproj.filters @@ -3340,6 +3340,9 @@ JUCE Modules\juce_gui_basics\widgets + + JUCE Modules\juce_gui_basics\widgets + JUCE Modules\juce_gui_basics\widgets diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index 52311596f3..dc294ba8e5 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -369,6 +369,7 @@ #include "widgets/juce_Slider.cpp" #include "widgets/juce_TableHeaderComponent.cpp" #include "widgets/juce_TableListBox.cpp" +#include "widgets/juce_TextEditorModel.cpp" #include "widgets/juce_TextEditor.cpp" #include "widgets/juce_Toolbar.cpp" #include "widgets/juce_ToolbarItemComponent.cpp" diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp index a8d01dd58d..37742feea7 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.cpp +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.cpp @@ -35,720 +35,6 @@ namespace juce { -// a word or space that can't be broken down any further -struct TextAtom -{ - //============================================================================== - String atomText; - float width; - int numChars; - - //============================================================================== - bool isWhitespace() const noexcept { return CharacterFunctions::isWhitespace (atomText[0]); } - bool isNewLine() const noexcept { return atomText[0] == '\r' || atomText[0] == '\n'; } - - String getText (juce_wchar passwordCharacter) const - { - if (passwordCharacter == 0) - return atomText; - - return String::repeatedString (String::charToString (passwordCharacter), - atomText.length()); - } - - String getTrimmedText (const juce_wchar passwordCharacter) const - { - if (passwordCharacter == 0) - return atomText.substring (0, numChars); - - if (isNewLine()) - return {}; - - return String::repeatedString (String::charToString (passwordCharacter), numChars); - } - - JUCE_LEAK_DETECTOR (TextAtom) -}; - -//============================================================================== -// a run of text with a single font and colour -class TextEditor::UniformTextSection -{ -public: - UniformTextSection (const String& text, const Font& f, Colour col, juce_wchar passwordCharToUse) - : font (f), colour (col), passwordChar (passwordCharToUse) - { - initialiseAtoms (text); - } - - UniformTextSection (const UniformTextSection&) = default; - UniformTextSection (UniformTextSection&&) = default; - - UniformTextSection& operator= (const UniformTextSection&) = delete; - - void append (UniformTextSection& other) - { - if (! other.atoms.isEmpty()) - { - int i = 0; - - if (! atoms.isEmpty()) - { - auto& lastAtom = atoms.getReference (atoms.size() - 1); - - if (! CharacterFunctions::isWhitespace (lastAtom.atomText.getLastCharacter())) - { - auto& first = other.atoms.getReference (0); - - if (! CharacterFunctions::isWhitespace (first.atomText[0])) - { - lastAtom.atomText += first.atomText; - lastAtom.numChars = (uint16) (lastAtom.numChars + first.numChars); - lastAtom.width = GlyphArrangement::getStringWidth (font, lastAtom.getText (passwordChar)); - ++i; - } - } - } - - atoms.ensureStorageAllocated (atoms.size() + other.atoms.size() - i); - - while (i < other.atoms.size()) - { - atoms.add (other.atoms.getReference (i)); - ++i; - } - } - } - - UniformTextSection* split (int indexToBreakAt) - { - auto* section2 = new UniformTextSection ({}, font, colour, passwordChar); - int index = 0; - - for (int i = 0; i < atoms.size(); ++i) - { - auto& atom = atoms.getReference (i); - auto nextIndex = index + atom.numChars; - - if (index == indexToBreakAt) - { - for (int j = i; j < atoms.size(); ++j) - section2->atoms.add (atoms.getUnchecked (j)); - - atoms.removeRange (i, atoms.size()); - break; - } - - if (indexToBreakAt >= index && indexToBreakAt < nextIndex) - { - TextAtom secondAtom; - secondAtom.atomText = atom.atomText.substring (indexToBreakAt - index); - secondAtom.width = GlyphArrangement::getStringWidth (font, secondAtom.getText (passwordChar)); - secondAtom.numChars = (uint16) secondAtom.atomText.length(); - - section2->atoms.add (secondAtom); - - atom.atomText = atom.atomText.substring (0, indexToBreakAt - index); - atom.width = GlyphArrangement::getStringWidth (font, atom.getText (passwordChar)); - atom.numChars = (uint16) (indexToBreakAt - index); - - for (int j = i + 1; j < atoms.size(); ++j) - section2->atoms.add (atoms.getUnchecked (j)); - - atoms.removeRange (i + 1, atoms.size()); - break; - } - - index = nextIndex; - } - - return section2; - } - - void appendAllText (MemoryOutputStream& mo) const - { - for (auto& atom : atoms) - mo << atom.atomText; - } - - void appendSubstring (MemoryOutputStream& mo, Range range) const - { - int index = 0; - - for (auto& atom : atoms) - { - auto nextIndex = index + atom.numChars; - - if (range.getStart() < nextIndex) - { - if (range.getEnd() <= index) - break; - - auto r = (range - index).getIntersectionWith ({ 0, (int) atom.numChars }); - - if (! r.isEmpty()) - mo << atom.atomText.substring (r.getStart(), r.getEnd()); - } - - index = nextIndex; - } - } - - int getTotalLength() const noexcept - { - int total = 0; - - for (auto& atom : atoms) - total += atom.numChars; - - return total; - } - - void setFont (const Font& newFont, const juce_wchar passwordCharToUse) - { - if (font != newFont || passwordChar != passwordCharToUse) - { - font = newFont; - passwordChar = passwordCharToUse; - - for (auto& atom : atoms) - atom.width = GlyphArrangement::getStringWidth (newFont, atom.getText (passwordChar)); - } - } - - //============================================================================== - Font font; - Colour colour; - Array atoms; - juce_wchar passwordChar; - -private: - void initialiseAtoms (const String& textToParse) - { - auto text = textToParse.getCharPointer(); - - while (! text.isEmpty()) - { - size_t numChars = 0; - auto start = text; - - // create a whitespace atom unless it starts with non-ws - if (text.isWhitespace() && *text != '\r' && *text != '\n') - { - do - { - ++text; - ++numChars; - } - while (text.isWhitespace() && *text != '\r' && *text != '\n'); - } - else - { - if (*text == '\r') - { - ++text; - ++numChars; - - if (*text == '\n') - { - ++start; - ++text; - } - } - else if (*text == '\n') - { - ++text; - ++numChars; - } - else - { - while (! (text.isEmpty() || text.isWhitespace())) - { - ++text; - ++numChars; - } - } - } - - TextAtom atom; - atom.atomText = String (start, numChars); - atom.width = (atom.isNewLine() ? 0.0f : GlyphArrangement::getStringWidth (font, atom.getText (passwordChar))); - atom.numChars = (uint16) numChars; - atoms.add (atom); - } - } - - JUCE_LEAK_DETECTOR (UniformTextSection) -}; - -//============================================================================== -struct TextEditor::Iterator -{ - Iterator (const TextEditor& ed) - : sections (ed.sections), - justification (ed.justification), - bottomRight ((float) ed.getMaximumTextWidth(), (float) ed.getMaximumTextHeight()), - wordWrapWidth ((float) ed.getWordWrapWidth()), - passwordCharacter (ed.passwordCharacter), - lineSpacing (ed.lineSpacing), - underlineWhitespace (ed.underlineWhitespace) - { - jassert (wordWrapWidth > 0); - - if (! sections.isEmpty()) - { - currentSection = sections.getUnchecked (sectionIndex); - - if (currentSection != nullptr) - beginNewLine(); - } - - lineHeight = ed.currentFont.getHeight(); - } - - Iterator (const Iterator&) = default; - Iterator& operator= (const Iterator&) = delete; - - //============================================================================== - bool next() - { - if (atom == &longAtom && chunkLongAtom (true)) - return true; - - if (sectionIndex >= sections.size()) - { - moveToEndOfLastAtom(); - return false; - } - - bool forceNewLine = false; - - if (atomIndex >= currentSection->atoms.size() - 1) - { - if (atomIndex >= currentSection->atoms.size()) - { - if (++sectionIndex >= sections.size()) - { - moveToEndOfLastAtom(); - return false; - } - - atomIndex = 0; - currentSection = sections.getUnchecked (sectionIndex); - } - else - { - auto& lastAtom = currentSection->atoms.getReference (atomIndex); - - if (! lastAtom.isWhitespace()) - { - // handle the case where the last atom in a section is actually part of the same - // word as the first atom of the next section... - float right = atomRight + lastAtom.width; - float lineHeight2 = lineHeight; - float maxDescent2 = maxDescent; - - for (int section = sectionIndex + 1; section < sections.size(); ++section) - { - auto* s = sections.getUnchecked (section); - - if (s->atoms.size() == 0) - break; - - auto& nextAtom = s->atoms.getReference (0); - - if (nextAtom.isWhitespace()) - break; - - right += nextAtom.width; - - lineHeight2 = jmax (lineHeight2, s->font.getHeight()); - maxDescent2 = jmax (maxDescent2, s->font.getDescent()); - - if (shouldWrap (right)) - { - lineHeight = lineHeight2; - maxDescent = maxDescent2; - - forceNewLine = true; - break; - } - - if (s->atoms.size() > 1) - break; - } - } - } - } - - bool isInPreviousAtom = false; - - if (atom != nullptr) - { - atomX = atomRight; - indexInText += atom->numChars; - - if (atom->isNewLine()) - beginNewLine(); - else - isInPreviousAtom = true; - } - - atom = &(currentSection->atoms.getReference (atomIndex)); - atomRight = atomX + atom->width; - ++atomIndex; - - if (shouldWrap (atomRight) || forceNewLine) - { - if (atom->isWhitespace()) - { - // leave whitespace at the end of a line, but truncate it to avoid scrolling - atomRight = jmin (atomRight, wordWrapWidth); - } - else if (shouldWrap (atom->width)) // atom too big to fit on a line, so break it up.. - { - longAtom = *atom; - longAtom.numChars = 0; - atom = &longAtom; - chunkLongAtom (isInPreviousAtom); - } - else - { - beginNewLine(); - atomRight = atomX + atom->width; - } - } - - return true; - } - - void beginNewLine() - { - lineY += lineHeight * lineSpacing; - float lineWidth = 0; - - auto tempSectionIndex = sectionIndex; - auto tempAtomIndex = atomIndex; - auto* section = sections.getUnchecked (tempSectionIndex); - - lineHeight = section->font.getHeight(); - maxDescent = section->font.getDescent(); - - float nextLineWidth = (atom != nullptr) ? atom->width : 0.0f; - - while (! shouldWrap (nextLineWidth)) - { - lineWidth = nextLineWidth; - - if (tempSectionIndex >= sections.size()) - break; - - bool checkSize = false; - - if (tempAtomIndex >= section->atoms.size()) - { - if (++tempSectionIndex >= sections.size()) - break; - - tempAtomIndex = 0; - section = sections.getUnchecked (tempSectionIndex); - checkSize = true; - } - - if (! isPositiveAndBelow (tempAtomIndex, section->atoms.size())) - break; - - auto& nextAtom = section->atoms.getReference (tempAtomIndex); - nextLineWidth += nextAtom.width; - - if (shouldWrap (nextLineWidth) || nextAtom.isNewLine()) - break; - - if (checkSize) - { - lineHeight = jmax (lineHeight, section->font.getHeight()); - maxDescent = jmax (maxDescent, section->font.getDescent()); - } - - ++tempAtomIndex; - } - - atomX = getJustificationOffsetX (lineWidth); - } - - float getJustificationOffsetX (float lineWidth) const - { - if (justification.testFlags (Justification::horizontallyCentred)) return jmax (0.0f, (bottomRight.x - lineWidth) * 0.5f); - if (justification.testFlags (Justification::right)) return jmax (0.0f, bottomRight.x - lineWidth); - - return 0; - } - - //============================================================================== - void draw (Graphics& g, const UniformTextSection*& lastSection, AffineTransform transform) const - { - if (atom == nullptr) - return; - - if (passwordCharacter != 0 || (underlineWhitespace || ! atom->isWhitespace())) - { - if (lastSection != currentSection) - { - lastSection = currentSection; - g.setColour (currentSection->colour); - g.setFont (currentSection->font); - } - - jassert (atom->getTrimmedText (passwordCharacter).isNotEmpty()); - - GlyphArrangement ga; - ga.addLineOfText (currentSection->font, - atom->getTrimmedText (passwordCharacter), - atomX, (float) roundToInt (lineY + lineHeight - maxDescent)); - ga.draw (g, transform); - } - } - - void drawUnderline (Graphics& g, Range underline, Colour colour, AffineTransform transform) const - { - auto startX = roundToInt (indexToX (underline.getStart())); - auto endX = roundToInt (indexToX (underline.getEnd())); - auto baselineY = roundToInt (lineY + currentSection->font.getAscent() + 0.5f); - - Graphics::ScopedSaveState state (g); - g.addTransform (transform); - g.reduceClipRegion ({ startX, baselineY, endX - startX, 1 }); - g.fillCheckerBoard ({ (float) endX, (float) baselineY + 1.0f }, 3.0f, 1.0f, colour, Colours::transparentBlack); - } - - void drawSelectedText (Graphics& g, Range selected, Colour selectedTextColour, AffineTransform transform) const - { - if (atom == nullptr) - return; - - if (passwordCharacter != 0 || ! atom->isWhitespace()) - { - GlyphArrangement ga; - ga.addLineOfText (currentSection->font, - atom->getTrimmedText (passwordCharacter), - atomX, (float) roundToInt (lineY + lineHeight - maxDescent)); - - if (selected.getEnd() < indexInText + atom->numChars) - { - GlyphArrangement ga2 (ga); - ga2.removeRangeOfGlyphs (0, selected.getEnd() - indexInText); - ga.removeRangeOfGlyphs (selected.getEnd() - indexInText, -1); - - g.setColour (currentSection->colour); - ga2.draw (g, transform); - } - - if (selected.getStart() > indexInText) - { - GlyphArrangement ga2 (ga); - ga2.removeRangeOfGlyphs (selected.getStart() - indexInText, -1); - ga.removeRangeOfGlyphs (0, selected.getStart() - indexInText); - - g.setColour (currentSection->colour); - ga2.draw (g, transform); - } - - g.setColour (selectedTextColour); - ga.draw (g, transform); - } - } - - //============================================================================== - float indexToX (int indexToFind) const - { - if (indexToFind <= indexInText || atom == nullptr) - return atomX; - - if (indexToFind >= indexInText + atom->numChars) - return atomRight; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, - atom->getText (passwordCharacter), - atomX, 0.0f); - - if (indexToFind - indexInText >= g.getNumGlyphs()) - return atomRight; - - return jmin (atomRight, g.getGlyph (indexToFind - indexInText).getLeft()); - } - - int xToIndex (float xToFind) const - { - if (xToFind <= atomX || atom == nullptr || atom->isNewLine()) - return indexInText; - - if (xToFind >= atomRight) - return indexInText + atom->numChars; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, - atom->getText (passwordCharacter), - atomX, 0.0f); - - auto numGlyphs = g.getNumGlyphs(); - - int j; - for (j = 0; j < numGlyphs; ++j) - { - auto& pg = g.getGlyph (j); - - if ((pg.getLeft() + pg.getRight()) / 2 > xToFind) - break; - } - - return indexInText + j; - } - - //============================================================================== - bool getCharPosition (int index, Point& anchor, float& lineHeightFound) - { - while (next()) - { - if (indexInText + atom->numChars > index) - { - anchor = { indexToX (index), lineY }; - lineHeightFound = lineHeight; - return true; - } - } - - anchor = { atomX, lineY }; - lineHeightFound = lineHeight; - return false; - } - - float getYOffset() - { - if (justification.testFlags (Justification::top) || lineY >= bottomRight.y) - return 0; - - while (next()) - { - if (lineY >= bottomRight.y) - return 0; - } - - auto bottom = jmax (0.0f, bottomRight.y - lineY - lineHeight); - - if (justification.testFlags (Justification::bottom)) - return bottom; - - return bottom * 0.5f; - } - - int getTotalTextHeight() - { - while (next()) {} - - auto height = lineY + lineHeight + getYOffset(); - - if (atom != nullptr && atom->isNewLine()) - height += lineHeight; - - return roundToInt (height); - } - - int getTextRight() - { - float maxWidth = 0.0f; - - while (next()) - maxWidth = jmax (maxWidth, atomRight); - - return roundToInt (maxWidth); - } - - Rectangle getTextBounds (Range range) const - { - auto startX = indexToX (range.getStart()); - auto endX = indexToX (range.getEnd()); - - return Rectangle (startX, lineY, endX - startX, lineHeight * lineSpacing).getSmallestIntegerContainer(); - } - - //============================================================================== - int indexInText = 0; - float lineY = 0, lineHeight = 0, maxDescent = 0; - float atomX = 0, atomRight = 0; - const TextAtom* atom = nullptr; - -private: - const OwnedArray& sections; - const UniformTextSection* currentSection = nullptr; - int sectionIndex = 0, atomIndex = 0; - Justification justification; - const Point bottomRight; - const float wordWrapWidth; - const juce_wchar passwordCharacter; - const float lineSpacing; - const bool underlineWhitespace; - TextAtom longAtom; - - bool chunkLongAtom (bool shouldStartNewLine) - { - const auto numRemaining = longAtom.atomText.length() - longAtom.numChars; - - if (numRemaining <= 0) - return false; - - longAtom.atomText = longAtom.atomText.substring (longAtom.numChars); - indexInText += longAtom.numChars; - - GlyphArrangement g; - g.addLineOfText (currentSection->font, atom->getText (passwordCharacter), 0.0f, 0.0f); - - int split; - for (split = 0; split < g.getNumGlyphs(); ++split) - if (shouldWrap (g.getGlyph (split).getRight())) - break; - - const auto numChars = jmax (1, split); - longAtom.numChars = (uint16) numChars; - longAtom.width = g.getGlyph (numChars - 1).getRight(); - - atomX = getJustificationOffsetX (longAtom.width); - - if (shouldStartNewLine) - { - if (split == numRemaining) - beginNewLine(); - else - lineY += lineHeight * lineSpacing; - } - - atomRight = atomX + longAtom.width; - return true; - } - - void moveToEndOfLastAtom() - { - if (atom != nullptr) - { - atomX = atomRight; - - if (atom->isNewLine()) - { - atomX = getJustificationOffsetX (0); - lineY += lineHeight * lineSpacing; - } - } - } - - bool shouldWrap (const float x) const noexcept - { - return (x - 0.0001f) >= wordWrapWidth; - } - - JUCE_LEAK_DETECTOR (Iterator) -}; - - //============================================================================== struct TextEditor::InsertAction final : public UndoableAction { @@ -794,44 +80,43 @@ private: //============================================================================== struct TextEditor::RemoveAction final : public UndoableAction { - RemoveAction (TextEditor& ed, Range rangeToRemove, int oldCaret, int newCaret, - const Array& oldSections) + RemoveAction (TextEditor& ed, Range rangeToRemove, int oldCaret, int newCaret) : owner (ed), range (rangeToRemove), oldCaretPos (oldCaret), newCaretPos (newCaret) { - removedSections.addArray (oldSections); } bool perform() override { - owner.remove (range, nullptr, newCaretPos); + owner.remove (range, nullptr, newCaretPos, &removedText); return true; } bool undo() override { - owner.reinsert (range.getStart(), removedSections); + owner.reinsert (removedText); owner.moveCaretTo (oldCaretPos, false); return true; } int getSizeInUnits() override { - int n = 16; - - for (auto* s : removedSections) - n += s->getTotalLength(); - - return n; + return std::accumulate (removedText.texts.begin(), + removedText.texts.end(), + 0, + [] (auto& sum, auto& value) + { + return sum + (int) value.getNumBytesAsUTF8(); + }); } private: TextEditor& owner; const Range range; const int oldCaretPos, newCaretPos; - OwnedArray removedSections; + TextEditorStorageChunks removedText; JUCE_DECLARE_NON_COPYABLE (RemoveAction) }; @@ -897,6 +182,7 @@ struct TextEditor::TextEditorViewport final : public Viewport // appear and disappear, causing the wrap width to change. { auto wordWrapWidth = owner.getWordWrapWidth(); + owner.updateBaseShapedTextOptions(); if (wordWrapWidth != lastWordWrapWidth) { @@ -941,7 +227,9 @@ namespace TextEditorDefs //============================================================================== TextEditor::TextEditor (const String& name, juce_wchar passwordChar) : Component (name), - passwordCharacter (passwordChar) + passwordCharacter (passwordChar), + textStorage { std::make_unique() }, + caretState { this } { setMouseCursor (MouseCursor::IBeamCursor); @@ -1006,6 +294,7 @@ void TextEditor::setMultiLine (const bool shouldBeMultiLine, { multiline = shouldBeMultiLine; wordWrap = shouldWordWrap && shouldBeMultiLine; + updateBaseShapedTextOptions(); checkLayout(); @@ -1100,15 +389,11 @@ void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont if (changeCurrentFont) currentFont = newFont; - auto overallColour = findColour (textColourId); + textStorage->setFontForAllText (newFont); - for (auto* uts : sections) - { - uts->setFont (newFont, passwordCharacter); - uts->colour = overallColour; - } + const auto overallColour = findColour (textColourId); + textStorage->setColourForAllText (overallColour); - coalesceSimilarSections(); checkLayout(); scrollToMakeSureCursorIsVisible(); repaint(); @@ -1116,8 +401,7 @@ void TextEditor::applyFontToAllText (const Font& newFont, bool changeCurrentFont void TextEditor::applyColourToAllText (const Colour& newColour, bool changeCurrentTextColour) { - for (auto* uts : sections) - uts->colour = newColour; + textStorage->setColourForAllText (newColour); if (changeCurrentTextColour) setColour (TextEditor::textColourId, newColour); @@ -1173,9 +457,8 @@ void TextEditor::updateCaretPosition() if (caret != nullptr && getWidth() > 0 && getHeight() > 0) { - Iterator i (*this); caret->setCaretPosition (getCaretRectangle().translated (leftIndent, - topIndent + roundToInt (i.getYOffset())) - getTextOffset()); + topIndent + roundToInt (getYOffset())) - getTextOffset()); if (auto* handler = getAccessibilityHandler()) handler->notifyAccessibilityEvent (AccessibilityEvent::textSelectionChanged); @@ -1222,6 +505,7 @@ void TextEditor::setPasswordCharacter (juce_wchar newPasswordCharacter) { passwordCharacter = newPasswordCharacter; applyFontToAllText (currentFont); + updateBaseShapedTextOptions(); } } @@ -1250,11 +534,11 @@ void TextEditor::setText (const String& newText, bool sendTextChangeMessage) textValue = newText; - auto oldCursorPos = caretPosition; + auto oldCursorPos = caretState.getPosition(); auto cursorWasAtEnd = oldCursorPos >= getTotalNumChars(); clearInternal (nullptr); - insert (newText, 0, currentFont, findColour (textColourId), nullptr, caretPosition); + insert (newText, 0, currentFont, findColour (textColourId), nullptr, caretState.getPosition()); // if you're adding text with line-feeds to a single-line text editor, it // ain't gonna look right! @@ -1362,11 +646,8 @@ void TextEditor::repaintText (Range range) return; } - Iterator i (*this); - - Point anchor; - auto lh = currentFont.getHeight(); - i.getCharPosition (range.getStart(), anchor, lh); + const auto [anchor, lh] = getCursorEdge (caretState.withPosition (range.getStart()) + .withPreferredEdge (Edge::trailing)); auto y1 = std::trunc (anchor.y); int y2 = 0; @@ -1377,12 +658,18 @@ void TextEditor::repaintText (Range range) } else { - i.getCharPosition (range.getEnd(), anchor, lh); - y2 = (int) (anchor.y + lh * 2.0f); + const auto info = getCursorEdge (caretState.withPosition (range.getEnd()) + .withPreferredEdge (Edge::leading)); + + y2 = (int) (info.first.y + lh * 2.0f); } - auto offset = i.getYOffset(); - textHolder->repaint (0, roundToInt (y1 + offset), textHolder->getWidth(), roundToInt ((float) y2 - y1 + offset)); + const auto offset = getYOffset(); + + textHolder->repaint (0, + (int) std::floor (y1 + offset), + textHolder->getWidth(), + (int) std::ceil ((float) y2 - y1 + offset)); } } @@ -1394,7 +681,7 @@ void TextEditor::moveCaret (const int newCaretPos) if (clamped == getCaretPosition()) return; - caretPosition = clamped; + caretState.setPosition (clamped); if (hasKeyboardFocus (false)) textHolder->restartTimer(); @@ -1408,7 +695,7 @@ void TextEditor::moveCaret (const int newCaretPos) int TextEditor::getCaretPosition() const { - return caretPosition; + return caretState.getPosition(); } void TextEditor::setCaretPosition (const int newIndex) @@ -1457,34 +744,211 @@ void TextEditor::scrollEditorToPositionCaret (const int desiredCaretX, Rectangle TextEditor::getCaretRectangleForCharIndex (int index) const { - Point anchor; - auto cursorHeight = currentFont.getHeight(); // (in case the text is empty and the call below doesn't set this value) - getCharPosition (index, anchor, cursorHeight); - - return Rectangle { anchor.x, anchor.y, 2.0f, cursorHeight }.getSmallestIntegerContainer() + getTextOffset(); + const auto [anchor, cursorHeight] = getCursorEdge (caretState.withPosition (index)); + Rectangle caretRectangle { anchor.x, anchor.y, 2.0f, cursorHeight }; + return caretRectangle.getSmallestIntegerContainer() + getTextOffset(); } -Point TextEditor::getTextOffset() const noexcept +Point TextEditor::getTextOffset() const { - Iterator i (*this); - auto yOffset = i.getYOffset(); - return { getLeftIndent() + borderSize.getLeft() - viewport->getViewPositionX(), - roundToInt ((float) getTopIndent() + (float) borderSize.getTop() + yOffset) - viewport->getViewPositionY() }; + roundToInt ((float) getTopIndent() + (float) borderSize.getTop() + getYOffset()) - viewport->getViewPositionY() }; +} + +template +detail::RangedValues TextEditor::getGlyphRanges (const detail::RangedValues& textRanges) const +{ + detail::RangedValues glyphRanges; + std::vector> glyphRangesStorage; + + detail::Ranges::Operations ops; + + for (const auto [range, value, paragraph] : makeIntersectingRangedValues (&textRanges, + textStorage.get())) + { + paragraph->getShapedText().getGlyphRanges (range - paragraph->getRange().getStart(), + glyphRangesStorage); + + for (const auto& glyphRange : glyphRangesStorage) + { + glyphRanges.set (glyphRange + paragraph->getStartingGlyph(), value, ops); + ops.clear(); + } + } + + return glyphRanges; +} + +bool TextEditor::isTextStorageHeightGreaterEqualThan (float value) const +{ + float height = 0.0; + + for (auto paragraphItem : *textStorage) + { + height += paragraphItem.value->getHeight(); + + if (height >= value) + return true; + } + + return false; +} + +float TextEditor::getTextStorageHeight() const +{ + const auto textHeight = std::accumulate (textStorage->begin(), textStorage->end(), 0.0f, [&] (auto acc, auto item) + { + return acc + item.value->getHeight(); + }); + + if (! textStorage->isEmpty() && ! textStorage->back().value->getText().endsWith ("\n")) + return textHeight; + + return textHeight + getLineSpacing() * textStorage->getLastFont().value_or (currentFont).getHeight(); +} + +float TextEditor::getYOffset() const +{ + const auto bottomY = getMaximumTextHeight(); + + if (justification.testFlags (Justification::top) || isTextStorageHeightGreaterEqualThan ((float) bottomY)) + return 0; + + auto bottom = jmax (0.0f, (float) bottomY - getTextStorageHeight()); + + if (justification.testFlags (Justification::bottom)) + return bottom; + + return bottom * 0.5f; +} + +Range TextEditor::getLineRangeForIndex (int index) +{ + jassert (index >= 0); + + const auto indexInText = (int64) index; + + if (textStorage->isEmpty()) + return { indexInText, indexInText }; + + if (const auto paragraph = textStorage->getParagraphContainingCodepointIndex (indexInText)) + { + const auto& shapedText = paragraph->value->getShapedText(); + auto r = *shapedText.getLineTextRanges().find (indexInText - paragraph->range.getStart()) + + paragraph->range.getStart(); + + if (r.getEnd() != paragraph->range.getEnd()) + return r; + + constexpr juce_wchar cr = 0x0d; + constexpr juce_wchar lf = 0x0a; + + const auto startIt = shapedText.getText().begin(); + auto endIt = shapedText.getText().end(); + + for (int i = 0; i < 2; ++i) + { + if (endIt == startIt) + break; + + auto newEnd = endIt - 1; + + if (*newEnd != cr && *newEnd != lf) + break; + + r.setEnd (std::max (r.getStart(), r.getEnd() - 1)); + endIt = newEnd; + } + + return r; + } + + const auto& lastParagraphItem = textStorage->back(); + + if (lastParagraphItem.value->getText().endsWith ("\n")) + return Range::withStartAndLength (lastParagraphItem.range.getEnd(), 0); + + return lastParagraphItem.value->getShapedText().getLineTextRanges().getRanges().back() + + lastParagraphItem.range.getStart(); +} + +std::pair, float> TextEditor::getTextSelectionEdge (int index, Edge edge) const +{ + jassert (0 <= index && index < getTotalNumChars()); + const auto textRange = Range::withStartAndLength ((int64) index, 1); + + const auto paragraphIt = std::find_if (textStorage->begin(), + textStorage->end(), + [&] (const auto& p) + { + return p.range.contains (textRange.getStart()); + }); + + jassert (paragraphIt != textStorage->end()); + + auto& paragraph = paragraphIt->value; + const auto& shapedText = paragraph->getShapedText(); + + const auto glyphRange = std::invoke ([&] + { + std::vector> g; + shapedText.getGlyphRanges (textRange - paragraph->getRange().getStart(), g); + jassert (! g.empty()); + return g.front(); + }); + + const auto glyphsBounds = shapedText.getGlyphsBounds (glyphRange).getRectangle (0); + const auto ltr = shapedText.isLtr (glyphRange.getStart()); + + const auto anchorX = std::invoke ([&] + { + if (edge == Edge::leading) + return ltr ? glyphsBounds.getX() : glyphsBounds.getRight(); + + return ltr ? glyphsBounds.getRight() : glyphsBounds.getX(); + }); + + const auto lineMetrics = shapedText.getLineMetricsForGlyphRange().find (glyphRange.getStart())->value; + const auto anchorY = lineMetrics.anchor.getY(); + + return { { anchorX, anchorY -lineMetrics.maxAscent + paragraph->getTop() }, + lineMetrics.maxAscent + lineMetrics.maxDescent }; +} + +void TextEditor::updateBaseShapedTextOptions() +{ + textStorage->setBaseShapedTextOptions (detail::ShapedText::Options{} + .withMaxWidth ((float) getWordWrapWidth()) + .withTrailingWhitespacesShouldFit (true) + .withJustification (getJustificationType().getOnlyHorizontalFlags()), + passwordCharacter); +} + +static auto asInt64Range (Range r) +{ + return Range { (int64) r.getStart(), (int64) r.getEnd() }; } RectangleList TextEditor::getTextBounds (Range textRange) const { RectangleList boundingBox; - Iterator i (*this); - while (i.next()) + detail::RangedValues mask; + detail::Ranges::Operations ops; + mask.set (asInt64Range (textRange), 0, ops); + + for (auto [_1, paragraph, _2] : makeIntersectingRangedValues (textStorage.get(), &mask)) { - if (textRange.intersects ({ i.indexInText, - i.indexInText + i.atom->numChars })) - { - boundingBox.add (i.getTextBounds (textRange)); - } + ignoreUnused (_1, _2); + auto& shapedText = paragraph->getShapedText(); + + std::vector> glyphRanges; + shapedText.getGlyphRanges (asInt64Range (textRange) - paragraph->getRange().getStart(), + glyphRanges); + + for (const auto& glyphRange : glyphRanges) + for (const auto& bounds : shapedText.getGlyphsBounds (glyphRange)) + boundingBox.add (bounds.withY (bounds.getY() + paragraph->getTop()).getSmallestIntegerContainer()); } boundingBox.offsetAll (getTextOffset()); @@ -1515,11 +979,29 @@ void TextEditor::checkLayout() { if (getWordWrapWidth() > 0) { - const auto textBottom = Iterator (*this).getTotalTextHeight() + topIndent; - const auto textRight = jmax (viewport->getMaximumVisibleWidth(), - Iterator (*this).getTextRight() + leftIndent + rightEdgeSpace); + const auto textBottom = topIndent + + (int) std::ceil (getYOffset() + getTextStorageHeight()); - textHolder->setSize (textRight, textBottom); + const auto maxTextWidth = std::accumulate (textStorage->begin(), textStorage->end(), 0.0f, [&](auto pMax, auto paragraph) + { + auto& shapedText = paragraph.value->getShapedText(); + + const auto paragraphWidth = std::accumulate (shapedText.getLineMetricsForGlyphRange().begin(), + shapedText.getLineMetricsForGlyphRange().end(), + 0.0f, + [&] (auto lMax, auto line) + { + return std::max (lMax, + line.value.effectiveLineLength); + }); + + return std::max (pMax, paragraphWidth); + }); + + const auto textRight = std::max (viewport->getMaximumVisibleWidth(), + (int) std::ceil (maxTextWidth) + leftIndent + rightEdgeSpace); + + textHolder->setSize (textRight, std::max (textBottom, viewport->getHeight())); viewport->setScrollBarsShown (scrollbarVisible && multiline && textBottom > viewport->getMaximumVisibleHeight(), scrollbarVisible && multiline && ! wordWrap && textRight > viewport->getMaximumVisibleWidth()); } @@ -1716,70 +1198,263 @@ void TextEditor::cut() } } +static void drawUnderline (Graphics& g, + Span glyphs, + Span> positions, + const Font& font, + const AffineTransform& transform, + bool underlineWhitespace) +{ + const auto lineThickness = font.getDescent() * 0.3f; + + const auto getLeft = [&] (const auto& iter) + { + return *(positions.begin() + std::distance (glyphs.begin(), iter)); + }; + + const auto getRight = [&] (const auto& iter) + { + if (iter == glyphs.end()) + return positions.back() + glyphs.back().advance; + + const auto p = *(positions.begin() + std::distance (glyphs.begin(), iter)); + return p + iter->advance; + }; + + for (auto it = glyphs.begin(), end = glyphs.end(); it != end;) + { + const auto adjacent = std::adjacent_find (it, + end, + [] (const auto& a, const auto& b) + { + return a.isWhitespace() != b.isWhitespace(); + }); + + if (! it->isWhitespace() || underlineWhitespace) + { + const auto left = getLeft (it); + const auto right = getRight (adjacent); + + Path p; + p.addRectangle (left.x, left.y + lineThickness * 2.0f, right.x - left.x, lineThickness); + g.fillPath (p, transform); + } + + it = adjacent + (adjacent == end ? 0 : 1);; + } +} + +struct UseClip +{ + bool clipAtBegin = false; + bool clipAtEnd = false; +}; + +// Glyphs can reach beyond the anchor - advance defined rectangle. We shouldn't use a clip unless +// we need to partially paint a ligature. +static UseClip getDrawableGlyphs (Span glyphs, + Span> positions, + std::vector& glyphIdsOut, + std::vector>& positionsOut) +{ + jassert (! glyphs.empty() && glyphs.size() == positions.size()); + + glyphIdsOut.clear(); + positionsOut.clear(); + + UseClip useClip; + + const auto& firstGlyph = glyphs.front(); + + if (firstGlyph.isPlaceholderForLigature()) + { + useClip.clipAtBegin = true; + glyphIdsOut.push_back ((uint16_t) firstGlyph.glyphId); + positionsOut.push_back (positions[0] - (float) firstGlyph.getDistanceFromLigature() * firstGlyph.advance); + } + + int remainingLigaturePlaceholders = 0; + + for (const auto [index, glyph] : enumerate (glyphs, size_t{})) + { + if (glyph.isLigature()) + remainingLigaturePlaceholders += glyph.getNumTrailingLigaturePlaceholders(); + else + remainingLigaturePlaceholders = std::max (0, remainingLigaturePlaceholders - 1); + + if (! glyph.isPlaceholderForLigature()) + { + glyphIdsOut.push_back ((uint16_t) glyph.glyphId); + positionsOut.push_back (positions[index]); + } + } + + useClip.clipAtEnd = remainingLigaturePlaceholders > 0; + return useClip; +} + //============================================================================== void TextEditor::drawContent (Graphics& g) { - if (getWordWrapWidth() > 0) + using namespace detail; + + g.setOrigin (leftIndent, topIndent); + float yOffset = getYOffset(); + + Graphics::ScopedSaveState ss (g); + + detail::Ranges::Operations ops; + + const auto glyphColours = getGlyphRanges (textStorage->getColours()); + + const auto selectedTextRanges = std::invoke ([&] { - g.setOrigin (leftIndent, topIndent); - auto clip = g.getClipBounds(); + detail::RangedValues rv; + rv.set (asInt64Range (selection), 1, ops); + return getGlyphRanges (rv); + }); - auto yOffset = Iterator (*this).getYOffset(); + const auto textSelectionMask = std::invoke ([&] + { + ops.clear(); - AffineTransform transform; + detail::RangedValues rv; + rv.set ({ 0, textStorage->getTotalNumGlyphs() }, 0, ops); + + for (const auto item : selectedTextRanges) + rv.set (item.range, item.value, ops); + + return rv; + }); + + const auto underlining = std::invoke ([&] + { + ops.clear(); + + detail::RangedValues rv; + rv.set ({ 0, textStorage->getTotalNumChars() }, 0, ops); + + for (const auto& underlined : underlinedSections) + rv.set (asInt64Range (underlined), 1, ops); + + return getGlyphRanges (rv); + }); + + const auto drawSelection = [&] (Span glyphs, + Span> positions, + Font font, + Range, + LineMetrics, + int) + { + g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f)); + g.fillRect ({ positions.front().translated (0.0f, -font.getAscent() + yOffset), + positions.back().translated (glyphs.back().advance.getX(), font.getDescent() + yOffset) }); + }; + + std::vector glyphIds; + std::vector> positionsForGlyphIds; + + const auto drawGlyphRuns = [&] (Span glyphs, + Span> positions, + Font font, + Range, + LineMetrics, + Colour colour, + int isSelected, + int hasTemporaryUnderlining) + { + auto& context = g.getInternalContext(); + + if (context.getFont() != font) + context.setFont (font); + + const auto transform = AffineTransform::translation (0.0f, yOffset); + + g.setColour (isSelected ? findColour (highlightedTextColourId) : colour); + + glyphIds.clear(); + positionsForGlyphIds.clear(); + + const auto useClip = getDrawableGlyphs (glyphs, positions, glyphIds, positionsForGlyphIds); - if (yOffset > 0) { - transform = AffineTransform::translation (0.0f, yOffset); - clip.setY (roundToInt ((float) clip.getY() - yOffset)); - } + // Graphics::ScopedSaveState doesn't restore the clipping regions + context.saveState(); + const ScopeGuard restoreGraphicsContext { [&context] { context.restoreState(); } }; - Iterator i (*this); - Colour selectedTextColour; - - if (! selection.isEmpty()) - { - selectedTextColour = findColour (highlightedTextColourId); - - g.setColour (findColour (highlightColourId).withMultipliedAlpha (hasKeyboardFocus (true) ? 1.0f : 0.5f)); - - auto boundingBox = getTextBounds (selection); - boundingBox.offsetAll (-getTextOffset()); - - g.fillPath (boundingBox.toPath(), transform); - } - - const UniformTextSection* lastSection = nullptr; - - while (i.next() && i.lineY < (float) clip.getBottom()) - { - if (i.lineY + i.lineHeight >= (float) clip.getY()) + if (useClip.clipAtBegin || useClip.clipAtEnd) { - if (selection.intersects ({ i.indexInText, i.indexInText + i.atom->numChars })) - { - i.drawSelectedText (g, selection, selectedTextColour, transform); - lastSection = nullptr; - } - else - { - i.draw (g, lastSection, transform); - } + const auto componentBoundsInDrawBasis = getLocalBounds().toFloat().transformedBy (transform.inverted()); + + // We don't really want to constrain the vertical clip, so we add/subtract a little extra, + // because clipping right at the line 0 will still result in a visible clip border with + // the below code. + const auto clipTop = componentBoundsInDrawBasis.getY() - 10.0f; + const auto clipBottom = componentBoundsInDrawBasis.getBottom() + 10.0f; + const auto clipX = useClip.clipAtBegin ? positions.front().getX() : 0.0f; + const auto clipRight = useClip.clipAtEnd ? positions.back().getX() + glyphs.back().advance.getX() + : (float) getRight(); + + const Rectangle clipRect { { clipX, clipTop }, { clipRight, clipBottom } }; + Path clipPath; + clipPath.addRectangle (clipRect); + context.clipToPath (clipPath, transform); } + + context.drawGlyphs (glyphIds, positionsForGlyphIds, transform); } - for (auto& underlinedSection : underlinedSections) + if (font.isUnderlined()) + drawUnderline (g, glyphs, positions, font, transform, isWhitespaceUnderlined()); + + if (hasTemporaryUnderlining) { - Iterator i2 (*this); + const auto startX = roundToInt (positions.front().getX()); + const auto endX = roundToInt (positions.back().getX() + glyphs.back().advance.getX()); + auto baselineY = roundToInt (positions.front().getY() + 0.5f); - while (i2.next() && i2.lineY < (float) clip.getBottom()) - { - if (i2.lineY + i2.lineHeight >= (float) clip.getY() - && underlinedSection.intersects ({ i2.indexInText, i2.indexInText + i2.atom->numChars })) - { - i2.drawUnderline (g, underlinedSection, findColour (textColourId), transform); - } - } + Graphics::ScopedSaveState state (g); + g.addTransform (transform); + g.reduceClipRegion ({ startX, baselineY, endX - startX, 1 }); + + g.fillCheckerBoard ({ (float) endX, (float) baselineY + 1.0f }, + 3.0f, + 1.0f, + colour, + Colours::transparentBlack); } + }; + + const auto clip = std::invoke ([&] + { + auto c = g.getClipBounds(); + c.setY (roundToInt ((float) c.getY() - yOffset)); + return c; + }); + + for (auto [range, paragraph] : *textStorage) + { + ignoreUnused (range); + + const auto glyphsRange = Range::withStartAndLength (paragraph->getStartingGlyph(), + paragraph->getNumGlyphs()); + + const auto top = paragraph->getTop(); + const auto bottom = top + paragraph->getHeight(); + + if ((float) clip.getY() <= bottom && top <= (float) clip.getBottom()) + { + paragraph->getShapedText().accessTogetherWith (drawSelection, + selectedTextRanges.getIntersectionsStartingAtZeroWith (glyphsRange)); + + paragraph->getShapedText().accessTogetherWith (drawGlyphRuns, + glyphColours, + textSelectionMask.getIntersectionsStartingAtZeroWith (glyphsRange), + underlining.getIntersectionsStartingAtZeroWith (glyphsRange)); + } + + yOffset += paragraph->getHeight(); } } @@ -1858,6 +1533,7 @@ void TextEditor::mouseDown (const MouseEvent& e) { if (! (popupMenuEnabled && e.mods.isPopupMenu())) { + caretState.setPreferredEdge (Edge::leading); moveCaretTo (getTextIndexAt (e.getPosition()), e.mods.isShiftDown()); if (auto* peer = getPeer()) @@ -1889,8 +1565,13 @@ void TextEditor::mouseDown (const MouseEvent& e) void TextEditor::mouseDrag (const MouseEvent& e) { if (wasFocused || ! selectAllTextWhenFocused) + { if (! (popupMenuEnabled && e.mods.isPopupMenu())) + { + caretState.setPreferredEdge (Edge::leading); moveCaretTo (getTextIndexAt (e.getPosition()), true); + } + } } void TextEditor::mouseUp (const MouseEvent& e) @@ -2013,6 +1694,19 @@ bool TextEditor::moveCaretRight (bool moveInWholeWordSteps, bool selecting) return moveCaretWithTransaction (pos, selecting); } +TextEditor::Edge TextEditor::getEdgeTypeCloserToPosition (int indexInText, Point pos) const +{ + const auto testCaret = caretState.withPosition (indexInText); + + const auto leading = getCursorEdge (testCaret.withPreferredEdge (Edge::leading)).first.getDistanceFrom (pos); + const auto trailing = getCursorEdge (testCaret.withPreferredEdge (Edge::trailing)).first.getDistanceFrom (pos); + + if (leading < trailing) + return Edge::leading; + + return Edge::trailing; +} + bool TextEditor::moveCaretUp (bool selecting) { if (! isMultiLine()) @@ -2025,7 +1719,12 @@ bool TextEditor::moveCaretUp (bool selecting) if (newY < 0.0f) return moveCaretToStartOfLine (selecting); - return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), newY), selecting); + const Point testPosition { caretPos.getX(), newY }; + const auto newIndex = indexAtPosition (testPosition.getX(), testPosition.getY()); + + const auto edgeToUse = getEdgeTypeCloserToPosition (newIndex, testPosition); + caretState.setPreferredEdge (edgeToUse); + return moveCaretWithTransaction (newIndex, selecting); } bool TextEditor::moveCaretDown (bool selecting) @@ -2034,7 +1733,12 @@ bool TextEditor::moveCaretDown (bool selecting) return moveCaretToEndOfLine (selecting); const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat(); - return moveCaretWithTransaction (indexAtPosition (caretPos.getX(), caretPos.getBottom() + 1.0f), selecting); + const Point testPosition { caretPos.getX(), caretPos.getBottom() + 1.0f }; + const auto newIndex = indexAtPosition (testPosition.getX(), testPosition.getY()); + + const auto edgeToUse = getEdgeTypeCloserToPosition (newIndex, testPosition); + caretState.setPreferredEdge (edgeToUse); + return moveCaretWithTransaction (newIndex, selecting); } bool TextEditor::pageUp (bool selecting) @@ -2079,8 +1783,9 @@ bool TextEditor::moveCaretToTop (bool selecting) bool TextEditor::moveCaretToStartOfLine (bool selecting) { - const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat(); - return moveCaretWithTransaction (indexAtPosition (0.0f, caretPos.getCentreY()), selecting); + const auto lineRange = getLineRangeForIndex (caretState.getVisualIndex()); + caretState.setPreferredEdge (Edge::leading); + return moveCaretWithTransaction ((int) lineRange.getStart(), selecting); } bool TextEditor::moveCaretToEnd (bool selecting) @@ -2090,8 +1795,9 @@ bool TextEditor::moveCaretToEnd (bool selecting) bool TextEditor::moveCaretToEndOfLine (bool selecting) { - const auto caretPos = (getCaretRectangle() - getTextOffset()).toFloat(); - return moveCaretWithTransaction (indexAtPosition ((float) textHolder->getWidth(), caretPos.getCentreY()), selecting); + const auto lineRange = getLineRangeForIndex (caretState.getVisualIndex()); + caretState.setPreferredEdge (Edge::trailing); + return moveCaretWithTransaction ((int) lineRange.getEnd(), selecting); } bool TextEditor::deleteBackwards (bool moveInWholeWordSteps) @@ -2254,6 +1960,7 @@ void TextEditor::resized() { viewport->setBoundsInset (borderSize); viewport->setSingleStepSizes (16, roundToInt (currentFont.getHeight())); + updateBaseShapedTextOptions(); checkLayout(); @@ -2327,7 +2034,7 @@ UndoManager* TextEditor::getUndoManager() noexcept void TextEditor::clearInternal (UndoManager* const um) { - remove ({ 0, getTotalNumChars() }, um, caretPosition); + remove ({ 0, getTotalNumChars() }, um, getCaretPosition()); } void TextEditor::insert (const String& text, int insertIndex, const Font& font, @@ -2341,40 +2048,16 @@ void TextEditor::insert (const String& text, int insertIndex, const Font& font, newTransaction(); um->perform (new InsertAction (*this, text, insertIndex, font, colour, - caretPosition, caretPositionToMoveTo)); + getCaretPosition(), caretPositionToMoveTo)); } else { + textStorage->set ({ insertIndex, insertIndex }, text, font, colour); + caretState.updateEdge(); + repaintText ({ insertIndex, getTotalNumChars() }); // must do this before and after changing the data, in case // a line gets moved due to word wrap - int index = 0; - int nextIndex = 0; - - for (int i = 0; i < sections.size(); ++i) - { - nextIndex = index + sections.getUnchecked (i)->getTotalLength(); - - if (insertIndex == index) - { - sections.insert (i, new UniformTextSection (text, font, colour, passwordCharacter)); - break; - } - - if (insertIndex > index && insertIndex < nextIndex) - { - splitSection (i, insertIndex - index); - sections.insert (i + 1, new UniformTextSection (text, font, colour, passwordCharacter)); - break; - } - - index = nextIndex; - } - - if (nextIndex == insertIndex) - sections.add (new UniformTextSection (text, font, colour, passwordCharacter)); - - coalesceSimilarSections(); totalNumChars = -1; valueTextNeedsUpdating = true; @@ -2386,125 +2069,35 @@ void TextEditor::insert (const String& text, int insertIndex, const Font& font, } } -void TextEditor::reinsert (int insertIndex, const OwnedArray& sectionsToInsert) +void TextEditor::reinsert (const TextEditorStorageChunks& chunks) { - int index = 0; - int nextIndex = 0; - - for (int i = 0; i < sections.size(); ++i) - { - nextIndex = index + sections.getUnchecked (i)->getTotalLength(); - - if (insertIndex == index) - { - for (int j = sectionsToInsert.size(); --j >= 0;) - sections.insert (i, new UniformTextSection (*sectionsToInsert.getUnchecked (j))); - - break; - } - - if (insertIndex > index && insertIndex < nextIndex) - { - splitSection (i, insertIndex - index); - - for (int j = sectionsToInsert.size(); --j >= 0;) - sections.insert (i + 1, new UniformTextSection (*sectionsToInsert.getUnchecked (j))); - - break; - } - - index = nextIndex; - } - - if (nextIndex == insertIndex) - for (auto* s : sectionsToInsert) - sections.add (new UniformTextSection (*s)); - - coalesceSimilarSections(); + textStorage->addChunks (chunks); totalNumChars = -1; valueTextNeedsUpdating = true; } -void TextEditor::remove (Range range, UndoManager* const um, const int caretPositionToMoveTo) +void TextEditor::remove (Range range, + UndoManager* const um, + const int caretPositionToMoveTo, + TextEditorStorageChunks* removedOut) { + using namespace detail; + if (! range.isEmpty()) { - int index = 0; - - for (int i = 0; i < sections.size(); ++i) - { - auto nextIndex = index + sections.getUnchecked (i)->getTotalLength(); - - if (range.getStart() > index && range.getStart() < nextIndex) - { - splitSection (i, range.getStart() - index); - --i; - } - else if (range.getEnd() > index && range.getEnd() < nextIndex) - { - splitSection (i, range.getEnd() - index); - --i; - } - else - { - index = nextIndex; - - if (index > range.getEnd()) - break; - } - } - - index = 0; - if (um != nullptr) { - Array removedSections; - - for (auto* section : sections) - { - if (range.getEnd() <= range.getStart()) - break; - - auto nextIndex = index + section->getTotalLength(); - - if (range.getStart() <= index && range.getEnd() >= nextIndex) - removedSections.add (new UniformTextSection (*section)); - - index = nextIndex; - } - if (um->getNumActionsInCurrentTransaction() > TextEditorDefs::maxActionsPerTransaction) newTransaction(); - um->perform (new RemoveAction (*this, range, caretPosition, - caretPositionToMoveTo, removedSections)); + um->perform (new RemoveAction (*this, range, caretState.getPosition(), + caretPositionToMoveTo)); } else { - auto remainingRange = range; + textStorage->remove (asInt64Range (range), removedOut); + caretState.updateEdge(); - for (int i = 0; i < sections.size(); ++i) - { - auto* section = sections.getUnchecked (i); - auto nextIndex = index + section->getTotalLength(); - - if (remainingRange.getStart() <= index && remainingRange.getEnd() >= nextIndex) - { - sections.remove (i); - remainingRange.setEnd (remainingRange.getEnd() - (nextIndex - index)); - - if (remainingRange.isEmpty()) - break; - - --i; - } - else - { - index = nextIndex; - } - } - - coalesceSimilarSections(); totalNumChars = -1; valueTextNeedsUpdating = true; @@ -2519,41 +2112,12 @@ void TextEditor::remove (Range range, UndoManager* const um, const int care //============================================================================== String TextEditor::getText() const { - MemoryOutputStream mo; - mo.preallocate ((size_t) getTotalNumChars()); - - for (auto* s : sections) - s->appendAllText (mo); - - return mo.toUTF8(); + return textStorage->getText(); } String TextEditor::getTextInRange (const Range& range) const { - if (range.isEmpty()) - return {}; - - MemoryOutputStream mo; - mo.preallocate ((size_t) jmin (getTotalNumChars(), range.getLength())); - - int index = 0; - - for (auto* s : sections) - { - auto nextIndex = index + s->getTotalLength(); - - if (range.getStart() < nextIndex) - { - if (range.getEnd() <= index) - break; - - s->appendSubstring (mo, range - index); - } - - index = nextIndex; - } - - return mo.toUTF8(); + return textStorage->getTextInRange (asInt64Range (range)); } String TextEditor::getHighlightedText() const @@ -2563,15 +2127,7 @@ String TextEditor::getHighlightedText() const int TextEditor::getTotalNumChars() const { - if (totalNumChars < 0) - { - totalNumChars = 0; - - for (auto* s : sections) - totalNumChars += s->getTotalLength(); - } - - return totalNumChars; + return (int) textStorage->getTotalNumChars(); } bool TextEditor::isEmpty() const @@ -2579,50 +2135,60 @@ bool TextEditor::isEmpty() const return getTotalNumChars() == 0; } -void TextEditor::getCharPosition (int index, Point& anchor, float& lineHeight) const +std::pair, float> TextEditor::getCursorEdge (const CaretState& tempCaret) const { - if (getWordWrapWidth() <= 0) - { - anchor = {}; - lineHeight = currentFont.getHeight(); - } - else - { - Iterator i (*this); + const auto visualIndex = tempCaret.getVisualIndex(); + jassert (0 <= visualIndex && visualIndex <= getTotalNumChars()); - if (sections.isEmpty()) - { - anchor = { i.getJustificationOffsetX (0), 0 }; - lineHeight = currentFont.getHeight(); - } - else - { - i.getCharPosition (index, anchor, lineHeight); - } - } + if (getWordWrapWidth() <= 0) + return { {}, currentFont.getHeight() }; + + const auto getJustificationOffsetX = [&] + { + const auto bottomRightX = (float) getMaximumTextWidth(); + + if (justification.testFlags (Justification::horizontallyCentred)) return jmax (0.0f, bottomRightX * 0.5f); + if (justification.testFlags (Justification::right)) return jmax (0.0f, bottomRightX); + + return 0.0f; + }; + + if (textStorage->isEmpty()) + return { { getJustificationOffsetX(), 0.0f }, currentFont.getHeight() }; + + return getTextSelectionEdge (visualIndex, tempCaret.getEdge()); } -int TextEditor::indexAtPosition (const float x, const float y) const +int TextEditor::indexAtPosition (float x, float y) const { - if (getWordWrapWidth() > 0) + y = std::max (0.0f, y); + + if (getWordWrapWidth() <= 0) + return getTotalNumChars(); + + auto paragraphIt = textStorage->begin(); + float paragraphTop = 0.0f; + + while (paragraphIt != textStorage->end()) { - for (Iterator i (*this); i.next();) - { - if (y < i.lineY + (i.lineHeight * lineSpacing)) - { - if (jmax (0.0f, y) < i.lineY) - return jmax (0, i.indexInText - 1); + auto& paragraph = paragraphIt->value; + const auto paragraphBottom = paragraphTop + paragraph->getHeight(); - if (x <= i.atomX || i.atom->isNewLine()) - return i.indexInText; + if (paragraphTop <= y && y < paragraphBottom) + break; - if (x < i.atomRight) - return i.xToIndex (x); - } - } + if (y < paragraphTop) + return {}; + + paragraphTop = paragraphBottom; + ++paragraphIt; } - return getTotalNumChars(); + if (paragraphIt == textStorage->end()) + return getTotalNumChars(); + + auto& shapedText = paragraphIt->value->getShapedText(); + return (int) (shapedText.getTextIndexForCaret ({ x, y - paragraphTop }) + paragraphIt->range.getStart()); } //============================================================================== @@ -2671,30 +2237,66 @@ int TextEditor::findWordBreakBefore (const int position) const return startOfBuffer + i; } - //============================================================================== -void TextEditor::splitSection (const int sectionIndex, const int charToSplitAt) +TextEditor::CaretState::CaretState (const TextEditor* ownerIn) + : owner { *ownerIn } { - jassert (sections[sectionIndex] != nullptr); - - sections.insert (sectionIndex + 1, - sections.getUnchecked (sectionIndex)->split (charToSplitAt)); + updateEdge(); } -void TextEditor::coalesceSimilarSections() +void TextEditor::CaretState::setPosition (int newPosition) { - for (int i = 0; i < sections.size() - 1; ++i) - { - auto* s1 = sections.getUnchecked (i); - auto* s2 = sections.getUnchecked (i + 1); + if (std::exchange (position, newPosition) != newPosition) + updateEdge(); +} - if (s1->font == s2->font - && s1->colour == s2->colour) - { - s1->append (*s2); - sections.remove (i + 1); - --i; - } +void TextEditor::CaretState::setPreferredEdge (TextEditor::Edge newEdge) +{ + if (std::exchange (preferredEdge, newEdge) != newEdge) + updateEdge(); +} + +int TextEditor::CaretState::getVisualIndex() const +{ + if (edge == Edge::leading) + return position; + + return position - 1; +} + +TextEditor::CaretState TextEditor::CaretState::withPosition (int newPosition) const +{ + auto copy = *this; + copy.setPosition (newPosition); + return copy; +} + +TextEditor::CaretState TextEditor::CaretState::withPreferredEdge (Edge newEdge) const +{ + auto copy = *this; + copy.setPreferredEdge (newEdge); + return copy; +} + +void TextEditor::CaretState::updateEdge() +{ + jassert (0 <= position && position <= owner.getTotalNumChars()); + + if (position == 0) + { + edge = Edge::leading; + } + else if (owner.getText()[position - 1] == '\n') + { + edge = Edge::leading; + } + else if (position == owner.getTotalNumChars()) + { + edge = Edge::trailing; + } + else + { + edge = preferredEdge; } } diff --git a/modules/juce_gui_basics/widgets/juce_TextEditor.h b/modules/juce_gui_basics/widgets/juce_TextEditor.h index 22401775f8..4b03eb493a 100644 --- a/modules/juce_gui_basics/widgets/juce_TextEditor.h +++ b/modules/juce_gui_basics/widgets/juce_TextEditor.h @@ -252,6 +252,9 @@ public: /** Applies a font to all the text in the editor. + This function also calls + applyColourToAllText (findColour (TextEditor::ColourIds::textColourId), false); + If the changeCurrentFont argument is true then this will also set the new font as the font to be used for any new text that's added. @@ -774,8 +777,6 @@ protected: private: //============================================================================== - JUCE_PUBLIC_IN_DLL_BUILD (class UniformTextSection) - struct Iterator; struct TextHolderComponent; struct TextEditorViewport; struct InsertAction; @@ -827,8 +828,51 @@ private: unsigned int lastTransactionTime = 0; Font currentFont { withDefaultMetrics (FontOptions { 14.0f }) }; mutable int totalNumChars = 0; - int caretPosition = 0; - OwnedArray sections; + + //============================================================================== + enum class Edge + { + leading, + trailing + }; + + //============================================================================== + struct CaretState + { + public: + explicit CaretState (const TextEditor* ownerIn); + + int getPosition() const { return position; } + Edge getEdge() const { return edge; } + + void setPosition (int newPosition); + + /* Not all visual edge positions are permitted e.g. a trailing caret after a newline + is not allowed. getVisualIndex() and getEdge() will return the closest permitted + values to the preferred one. + */ + void setPreferredEdge (Edge newEdge); + + /* The returned value is in the range [0, TextEditor::getTotalNumChars()]. It returns the + glyph index to which the caret is closest visually. This is significant when + differentiating between the end of one line and the beginning of the next. + */ + int getVisualIndex() const; + + void updateEdge(); + + //============================================================================== + CaretState withPosition (int newPosition) const; + CaretState withPreferredEdge (Edge newEdge) const; + + private: + const TextEditor& owner; + int position = 0; + Edge edge = Edge::trailing; + Edge preferredEdge = Edge::trailing; + }; + + //============================================================================== String textToShowWhenEmpty; Colour colourForTextWhenEmpty; juce_wchar passwordCharacter; @@ -849,17 +893,21 @@ private: ListenerList listeners; Array> underlinedSections; + class ParagraphStorage; + class ParagraphsModel; + struct TextEditorStorageChunks; + class TextEditorStorage; + void moveCaret (int newCaretPos); void moveCaretTo (int newPosition, bool isSelecting); void recreateCaret(); void handleCommandMessage (int) override; - void coalesceSimilarSections(); - void splitSection (int sectionIndex, int charToSplitAt); void clearInternal (UndoManager*); void insert (const String&, int insertIndex, const Font&, Colour, UndoManager*, int newCaretPos); - void reinsert (int insertIndex, const OwnedArray&); - void remove (Range, UndoManager*, int caretPositionToMoveTo); - void getCharPosition (int index, Point&, float& lineHeight) const; + void reinsert (const TextEditorStorageChunks& chunks); + void remove (Range, UndoManager*, int caretPositionToMoveTo, TextEditorStorageChunks* removedOut = nullptr); + std::pair, float> getTextSelectionEdge (int index, Edge edge) const; + std::pair, float> getCursorEdge (const CaretState& caret) const; void updateCaretPosition(); void updateValueFromText(); void textWasChangedByValue(); @@ -879,7 +927,21 @@ private: bool undoOrRedo (bool shouldUndo); UndoManager* getUndoManager() noexcept; void setSelection (Range) noexcept; - Point getTextOffset() const noexcept; + Point getTextOffset() const; + + Edge getEdgeTypeCloserToPosition (int indexInText, Point pos) const; + + std::unique_ptr textStorage; + CaretState caretState; + + bool isTextStorageHeightGreaterEqualThan (float value) const; + float getTextStorageHeight() const; + float getYOffset() const; + void updateBaseShapedTextOptions(); + Range getLineRangeForIndex (int index); + + template + detail::RangedValues getGlyphRanges (const detail::RangedValues& textRanges) const; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TextEditor) }; diff --git a/modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp b/modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp new file mode 100644 index 0000000000..78c77af74e --- /dev/null +++ b/modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp @@ -0,0 +1,572 @@ +/* + ============================================================================== + + This file is part of the JUCE framework. + Copyright (c) Raw Material Software Limited + + JUCE is an open source framework subject to commercial or open source + licensing. + + By downloading, installing, or using the JUCE framework, or combining the + JUCE framework with any other source code, object code, content or any other + copyrightable work, you agree to the terms of the JUCE End User Licence + Agreement, and all incorporated terms including the JUCE Privacy Policy and + the JUCE Website Terms of Service, as applicable, which will bind you. If you + do not agree to the terms of these agreements, we will not license the JUCE + framework to you, and you must discontinue the installation or download + process and cease use of the JUCE framework. + + JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ + JUCE Privacy Policy: https://juce.com/juce-privacy-policy + JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ + + Or: + + You may also use this code under the terms of the AGPLv3: + https://www.gnu.org/licenses/agpl-3.0.en.html + + THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL + WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +class TextEditor::ParagraphStorage +{ +public: + ParagraphStorage (String s, const TextEditorStorage* storageIn) + : text { std::move (s) }, + numBytesAsUTF8 { text.getNumBytesAsUTF8() }, + storage { *storageIn } + { + updatePasswordReplacementText(); + } + + const String& getText() const + { + return text; + } + + const String& getTextForDisplay() const; + + size_t getNumBytesAsUTF8() const; + + void setRange (Range rangeIn) + { + range = rangeIn; + } + + auto getRange() const + { + return range; + } + + const detail::ShapedText& getShapedText(); + + float getHeight() + { + if (! height.has_value()) + height = getShapedText().getHeight(); + + return *height; + } + + int64 getNumGlyphs() + { + if (! numGlyphs.has_value()) + numGlyphs = getShapedText().getNumGlyphs(); + + return *numGlyphs; + } + + float getTop(); + + int64 getStartingGlyph(); + + void clearShapedText(); + +private: + void updatePasswordReplacementText(); + + String text; + std::optional passwordReplacementText; + size_t numBytesAsUTF8; + Range range; + const TextEditorStorage& storage; + std::optional shapedText; + std::optional height; + std::optional numGlyphs; +}; + +//============================================================================== +class TextEditor::ParagraphsModel +{ +public: + using ParagraphItem = detail::RangedValuesIteratorItem>; + + explicit ParagraphsModel (const TextEditorStorage* ownerIn) + : owner { *ownerIn } + {} + + void set (Range range, const String& text) + { + using namespace detail; + + const auto codepointBeforeRange = getTextInRange (Range::withStartAndLength (range.getStart() - 1, 1)); + + Ranges::Operations ops; + + ranges.drop (range, ops); + + if (! text.isEmpty()) + { + ranges.insert ({ range.getStart(), range.getStart() + text.length() }, ops); + mergeForward (ranges, *ranges.getIndexForEnclosingRange (range.getStart()), ops); + } + + if (const auto newParagraphIndex = ranges.getIndexForEnclosingRange (range.getStart())) + ranges.mergeBack (*newParagraphIndex, ops); + + const auto splitBeforeOffset = range.getStart() + 1 - (codepointBeforeRange.isEmpty() ? 0 : 1); + + for (auto breakAfterIndex : UnicodeHelpers::getLineBreaks (codepointBeforeRange + text)) + ranges.split (breakAfterIndex + splitBeforeOffset, ops); + + handleOps (ops, text); + } + + String getText() const + { + const auto numBytes = std::accumulate (storage.begin(), + storage.end(), + (size_t) 0, + [] (auto sum, auto& p) + { + return sum + p->getNumBytesAsUTF8(); + }); + + MemoryOutputStream mo; + mo.preallocate (numBytes); + + for (const auto& paragraph : storage) + mo << paragraph->getText(); + + return mo.toUTF8(); + } + + String getTextInRange (Range range) const + { + String text; + + for (const auto& partialRange : ranges.getIntersectionsWith (range)) + { + const auto i = *ranges.getIndexForEnclosingRange (partialRange.getStart()); + const auto fullRange = ranges.get (i); + auto& paragraph = *storage[i]; + const auto startInParagraph = (int) (partialRange.getStart() - fullRange.getStart()); + text += paragraph.getText().substring (startInParagraph, + startInParagraph + (int) partialRange.getLength()); + } + + return text; + } + + auto begin() const + { + return detail::RangedValuesIterator> { storage.data(), + ranges.data(), + ranges.data() }; + } + + auto end() const + { + return detail::RangedValuesIterator> { storage.data(), + ranges.data(), + ranges.data() + ranges.size() }; + } + + std::optional getParagraphContainingCodepointIndex (int64 index) + { + const auto paragraphIndex = ranges.getIndexForEnclosingRange (index); + + if (! paragraphIndex.has_value()) + return std::nullopt; + + return ParagraphItem { ranges.get (*paragraphIndex), + storage[*paragraphIndex] }; + } + + bool isEmpty() const { return storage.empty(); } + + ParagraphItem back() const + { + jassert (! ranges.isEmpty()); + return { ranges.get (ranges.size() - 1), storage.back() }; + } + + int64 getTotalNumChars() const + { + if (ranges.isEmpty()) + return 0; + + return ranges.getRanges().back().getEnd(); + } + + int64 getTotalNumGlyphs() const + { + return std::accumulate (storage.begin(), + storage.end(), + (int64) 0, + [] (const auto& sum, const auto& item) + { + return sum + item->getNumGlyphs(); + }); + } + +private: + static void mergeForward (detail::Ranges& ranges, size_t index, detail::Ranges::Operations& ops) + { + if (ranges.size() > index + 1) + return ranges.mergeBack (index + 1, ops); + } + + void handleOps (const detail::Ranges::Operations& ops, const String& text) + { + using namespace detail; + + for (const auto& op : ops) + { + if (auto* newOp = std::get_if (&op)) + { + storage.insert (iteratorWithAdvance (storage.begin(), newOp->index), + createParagraph (text)); + } + else if (auto* split = std::get_if (&op)) + { + const auto& splitValue = storage[split->index]->getText(); + const auto localLeftRange = split->leftRange.movedToStartAt (0); + const auto localRightRange = split->rightRange.movedToStartAt (localLeftRange.getEnd()); + + auto leftSplitValue = splitValue.substring ((int) localLeftRange.getStart(), + (int) localLeftRange.getEnd()); + + auto rightSplitValue = splitValue.substring ((int) localRightRange.getStart(), + (int) localRightRange.getEnd()); + + storage[split->index] = createParagraph (std::move (leftSplitValue)); + + storage.insert (iteratorWithAdvance (storage.begin(), split->index + 1), + createParagraph (std::move (rightSplitValue))); + } + else if (auto* erased = std::get_if (&op)) + { + storage.erase (iteratorWithAdvance (storage.begin(), erased->range.getStart()), + iteratorWithAdvance (storage.begin(), erased->range.getEnd())); + } + else if (auto* changed = std::get_if (&op)) + { + const auto oldRange = changed->oldRange; + const auto newRange = changed->newRange; + + // This happens when a range just gets shifted due to drop or insert operations + if (oldRange.getLength() == newRange.getLength()) + continue; + + auto deltaStart = (int) (newRange.getStart() - oldRange.getStart()); + auto deltaEnd = (int) (newRange.getEnd() - oldRange.getEnd()); + + auto& paragraph = storage[changed->index]; + const auto& oldText = paragraph->getText(); + + jassert (deltaStart >= 0); + + if (deltaEnd <= 0) + { + paragraph = createParagraph (oldText.substring (deltaStart, oldText.length() + deltaEnd)); + } + else + { + jassert (changed->index + 1 < storage.size()); + paragraph = createParagraph (oldText.substring (deltaStart, oldText.length()) + + storage[changed->index + 1]->getText().substring (0, deltaEnd)); + } + } + } + + for (const auto [index, range] : enumerate (ranges, size_t{})) + storage[index]->setRange (range); + } + + std::unique_ptr createParagraph (String s) const + { + return std::make_unique (s, &owner); + } + + const TextEditorStorage& owner; + detail::Ranges ranges; + std::vector> storage; +}; + +//============================================================================== +struct TextEditor::TextEditorStorageChunks +{ + std::vector positions; + std::vector texts; + std::vector fonts; + std::vector colours; +}; + +//============================================================================== +class TextEditor::TextEditorStorage +{ +public: + void set (Range range, const String& text, const Font& font, const Colour& colour) + { + paragraphs.set (range, text); + + detail::Ranges::Operations ops; + + fonts.drop (range, ops); + colours.drop (range, ops); + ops.clear(); + + const auto insertionRange = Range::withStartAndLength (range.getStart(), + (int64) text.length()); + fonts.insert (insertionRange, font, ops); + colours.insert (insertionRange, colour, ops); + } + + void setFontForAllText (const Font& font) + { + detail::Ranges::Operations ops; + fonts.set ({ 0, paragraphs.getTotalNumChars() }, font, ops); + clearShapedTexts(); + } + + void setColourForAllText (const Colour& colour) + { + detail::Ranges::Operations ops; + colours.set ({ 0, paragraphs.getTotalNumChars() }, colour, ops); + clearShapedTexts(); + } + + void remove (Range range, TextEditorStorageChunks* removedOut) + { + using namespace detail; + + detail::Ranges::Operations ops; + + RangedValues rangeConstraint; + rangeConstraint.set (range, 0, ops); + + if (removedOut != nullptr) + { + for (const auto [r, font, colour, _] : makeIntersectingRangedValues (&fonts, &colours, &rangeConstraint)) + { + ignoreUnused (_); + + removedOut->positions.push_back (r.getStart()); + removedOut->texts.push_back (getTextInRange (r)); + removedOut->fonts.push_back (font); + removedOut->colours.push_back (colour); + } + } + + paragraphs.set (range, ""); + ops.clear(); + fonts.drop (range, ops); + colours.drop (range, ops); + } + + void addChunks (const TextEditorStorageChunks& chunks) + { + for (size_t i = 0; i < chunks.positions.size(); ++i) + { + set (Range::withStartAndLength (chunks.positions[i], 0), + chunks.texts[i], + chunks.fonts[i], + chunks.colours[i]); + } + } + + String getText() const + { + return paragraphs.getText(); + } + + String getTextInRange (Range range) const + { + return paragraphs.getTextInRange (range); + } + + detail::RangedValues getFonts (Range range) const + { + return fonts.getIntersectionsStartingAtZeroWith (range); + } + + const auto& getColours() const + { + return colours; + } + + auto begin() const { return paragraphs.begin(); } + auto end() const { return paragraphs.end(); } + + auto isEmpty() const { return paragraphs.isEmpty(); } + auto back() const { return paragraphs.back(); } + + std::optional getLastFont() const + { + if (fonts.isEmpty()) + return std::nullopt; + + return fonts.back().value; + } + + int64 getTotalNumChars() const + { + return paragraphs.getTotalNumChars(); + } + + int64 getTotalNumGlyphs() const + { + return paragraphs.getTotalNumGlyphs(); + } + + void setBaseShapedTextOptions (detail::ShapedTextOptions options, juce_wchar passwordCharacterIn) + { + if (std::exchange (baseShapedTextOptions, options) != options) + clearShapedTexts(); + + if (std::exchange (passwordCharacter, passwordCharacterIn) != passwordCharacterIn) + clearShapedTexts(); + } + + detail::ShapedTextOptions getShapedTextOptions (Range range) const + { + return baseShapedTextOptions.withFonts (getFonts (range)); + } + + juce_wchar getPasswordCharacter() const + { + return passwordCharacter; + } + + auto getParagraphContainingCodepointIndex (int64 index) + { + return paragraphs.getParagraphContainingCodepointIndex (index); + } + +private: + void clearShapedTexts() + { + for (auto p : paragraphs) + p.value->clearShapedText(); + } + + detail::RangedValues fonts; + detail::RangedValues colours; + ParagraphsModel paragraphs { this }; + detail::ShapedTextOptions baseShapedTextOptions; + juce_wchar passwordCharacter = 0; +}; + +//============================================================================== +const String& TextEditor::ParagraphStorage::getTextForDisplay() const +{ + if (passwordReplacementText.has_value()) + return *passwordReplacementText; + + return text; +} + +size_t TextEditor::ParagraphStorage::getNumBytesAsUTF8() const +{ + return numBytesAsUTF8; +} + +const detail::ShapedText& TextEditor::ParagraphStorage::getShapedText() +{ + if (! shapedText.has_value()) + shapedText.emplace (getTextForDisplay(), storage.getShapedTextOptions (range)); + + return *shapedText; +} + +float TextEditor::ParagraphStorage::getTop() +{ + float top = 0.0f; + + for (const auto paragraphItem : storage) + { + if (paragraphItem.value.get() == this) + break; + + top += paragraphItem.value->getHeight(); + } + + return top; +} + +int64 TextEditor::ParagraphStorage::getStartingGlyph() +{ + int64 startingGlyph = 0; + + for (const auto paragraph : storage) + { + if (paragraph.value.get() == this) + break; + + startingGlyph += paragraph.value->getNumGlyphs(); + } + + return startingGlyph; +} + +void TextEditor::ParagraphStorage::updatePasswordReplacementText() +{ + const auto passwordChar = storage.getPasswordCharacter(); + + if (passwordChar == 0) + { + passwordReplacementText.reset(); + return; + } + + constexpr juce_wchar cr = 0x0d; + constexpr juce_wchar lf = 0x0a; + + const auto startIt = text.begin(); + auto endIt = text.end(); + + for (int i = 0; i < 2; ++i) + { + if (endIt == startIt) + break; + + auto newEnd = endIt - 1; + + if (*newEnd != cr && *newEnd != lf) + break; + + endIt = newEnd; + } + + passwordReplacementText = String::repeatedString (String::charToString (passwordChar), + (int) startIt.lengthUpTo (endIt)) + + String { endIt, text.end() }; +} + +void TextEditor::ParagraphStorage::clearShapedText() +{ + shapedText.reset(); + height.reset(); + numGlyphs.reset(); + updatePasswordReplacementText(); +} + +}