1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00
JUCE/modules/juce_gui_basics/widgets/juce_TextEditorModel.cpp

579 lines
18 KiB
C++

/*
==============================================================================
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<int64> 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<String> passwordReplacementText;
size_t numBytesAsUTF8;
Range<int64> range;
const TextEditorStorage& storage;
std::optional<detail::ShapedText> shapedText;
std::optional<float> height;
std::optional<int64> numGlyphs;
};
//==============================================================================
class TextEditor::ParagraphsModel
{
public:
using ParagraphItem = detail::RangedValuesIteratorItem<const std::unique_ptr<ParagraphStorage>>;
explicit ParagraphsModel (const TextEditorStorage* ownerIn)
: owner { *ownerIn }
{}
void set (Range<int64> range, const String& text)
{
using namespace detail;
const auto codepointBeforeRange = getTextInRange (Range<int64>::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<int64> 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<const std::unique_ptr<ParagraphStorage>> { storage.data(),
ranges.data(),
ranges.data() };
}
auto end() const
{
return detail::RangedValuesIterator<const std::unique_ptr<ParagraphStorage>> { storage.data(),
ranges.data(),
ranges.data() + ranges.size() };
}
std::optional<ParagraphItem> 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 front() const
{
jassert (! ranges.isEmpty());
return { ranges.get (0), storage.front() };
}
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<Ranges::Ops::New> (&op))
{
storage.insert (iteratorWithAdvance (storage.begin(), newOp->index),
createParagraph (text));
}
else if (auto* split = std::get_if<Ranges::Ops::Split> (&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<Ranges::Ops::Erase> (&op))
{
storage.erase (iteratorWithAdvance (storage.begin(), erased->range.getStart()),
iteratorWithAdvance (storage.begin(), erased->range.getEnd()));
}
else if (auto* changed = std::get_if<Ranges::Ops::Change> (&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<ParagraphStorage> createParagraph (String s) const
{
return std::make_unique<ParagraphStorage> (s, &owner);
}
const TextEditorStorage& owner;
detail::Ranges ranges;
std::vector<std::unique_ptr<ParagraphStorage>> storage;
};
//==============================================================================
struct TextEditor::TextEditorStorageChunks
{
std::vector<int64> positions;
std::vector<String> texts;
std::vector<Font> fonts;
std::vector<Colour> colours;
};
//==============================================================================
class TextEditor::TextEditorStorage
{
public:
void set (Range<int64> 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<int64>::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<int64> range, TextEditorStorageChunks* removedOut)
{
using namespace detail;
detail::Ranges::Operations ops;
RangedValues<int64> 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<int64>::withStartAndLength (chunks.positions[i], 0),
chunks.texts[i],
chunks.fonts[i],
chunks.colours[i]);
}
}
String getText() const
{
return paragraphs.getText();
}
String getTextInRange (Range<int64> range) const
{
return paragraphs.getTextInRange (range);
}
detail::RangedValues<Font> getFonts (Range<int64> 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 front() const { return paragraphs.front(); }
auto back() const { return paragraphs.back(); }
std::optional<Font> 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<int64> 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<Font> fonts;
detail::RangedValues<Colour> 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();
}
}