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();
+}
+
+}