From 3e6ae97e8909a1f79ea2acc593495d1f0d584603 Mon Sep 17 00:00:00 2001 From: Mehmet Soyturk Date: Fri, 17 Sep 2021 09:19:48 +0300 Subject: [PATCH 1/3] Optimize wrapping long lines in wxRichTextCtrl When one inserts one big line to a wxRichTextCtrl that is too long (say 300k words of average size 9), then wxRichTextCtrl could freeze for a few seconds. It could also freeze again when the control is resized (such that word wrapping is triggered again). Problem: `wxRichTextParagraph::AllocateLine(int pos)` can be called many times. Each call triggers `m_cachedLines.Item(pos)`, which traverses the linked list. As a result we get quadratic time complexity. In this commit, we improve the function by also caching the lines in a vector, which supports random access in O(1) time. --- include/wx/richtext/richtextbuffer.h | 1 + src/richtext/richtextbuffer.cpp | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/include/wx/richtext/richtextbuffer.h b/include/wx/richtext/richtextbuffer.h index 132667c50c..84a1fd7002 100644 --- a/include/wx/richtext/richtextbuffer.h +++ b/include/wx/richtext/richtextbuffer.h @@ -4654,6 +4654,7 @@ protected: // The lines that make up the wrapped paragraph wxRichTextLineList m_cachedLines; + wxVector m_cachedLinesVect; // Whether the paragraph is impacted by floating objects from above int m_impactedByFloatingObjects; diff --git a/src/richtext/richtextbuffer.cpp b/src/richtext/richtextbuffer.cpp index b331564165..cbe9cf677a 100644 --- a/src/richtext/richtextbuffer.cpp +++ b/src/richtext/richtextbuffer.cpp @@ -5697,6 +5697,7 @@ void wxRichTextParagraph::Copy(const wxRichTextParagraph& obj) void wxRichTextParagraph::ClearLines() { WX_CLEAR_LIST(wxRichTextLineList, m_cachedLines); + m_cachedLinesVect.clear(); } /// Get/set the object size for the given range. Returns false if the range @@ -6529,7 +6530,8 @@ wxRichTextLine* wxRichTextParagraph::AllocateLine(int pos) { if (pos < (int) m_cachedLines.GetCount()) { - wxRichTextLine* line = m_cachedLines.Item(pos)->GetData(); + assert(m_cachedLinesVect.size() == m_cachedLines.GetCount()); + wxRichTextLine* line = m_cachedLinesVect[pos]; line->Init(this); return line; } @@ -6537,6 +6539,7 @@ wxRichTextLine* wxRichTextParagraph::AllocateLine(int pos) { wxRichTextLine* line = new wxRichTextLine(this); m_cachedLines.Append(line); + m_cachedLinesVect.push_back(line); return line; } } @@ -6555,6 +6558,7 @@ bool wxRichTextParagraph::ClearUnusedLines(int lineCount) delete line; } } + m_cachedLinesVect.resize(lineCount); return true; } From 6d9288c12b5c1d37d67563464fd2e837ce7d5da8 Mon Sep 17 00:00:00 2001 From: Mehmet Soyturk Date: Fri, 17 Sep 2021 09:24:31 +0300 Subject: [PATCH 2/3] Further optimize wrapping long lines in wxRichTextCtrl When adding characters to a long word-wrapped line one by one, there was an hotspot in wxString(const wxString&) and wxString::replace. When rendering or getting caret position, each broken line from a long line of text was being processed separately. Before handling each line, some preprocessing is done: replace a character by another character and make uppercase if necessary. Before, this way happening for each line: * make a copy of the whole long string * preprocess that copy of long string * get the needed substring of it Now, we do this in most cases: * get the needed substring of the long string * preprocess that short string --- src/richtext/richtextbuffer.cpp | 99 ++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 26 deletions(-) diff --git a/src/richtext/richtextbuffer.cpp b/src/richtext/richtextbuffer.cpp index cbe9cf677a..6156bb2820 100644 --- a/src/richtext/richtextbuffer.cpp +++ b/src/richtext/richtextbuffer.cpp @@ -6819,21 +6819,57 @@ bool wxRichTextPlainText::Draw(wxDC& dc, wxRichTextDrawingContext& context, cons int offset = GetRange().GetStart(); - wxString str = m_text; - if (context.HasVirtualText(this)) + const bool allSelected = selectionRange.GetStart() <= range.GetStart() && selectionRange.GetEnd() >= range.GetEnd(); + const bool noneSelected = selectionRange.GetEnd() < range.GetStart() || selectionRange.GetStart() > range.GetEnd(); + + wxString stringChunk; + wxString stringWhole; + + // If nothing or everything is selected, our algorithm does not need stringWhole. It + // is enough to preprocess stringChunk only. + // In case of partial selection we need to preprocess stringWhole too. + if (allSelected || noneSelected) { - if (!context.GetVirtualText(this, str) || str.Length() != m_text.Length()) - str = m_text; + const wxString* pWholeString = &m_text; + if (context.HasVirtualText(this)) + { + if (context.GetVirtualText(this, stringWhole) && stringWhole.Length() == m_text.Length()) + pWholeString = &stringWhole; + } + + long len = range.GetLength(); + stringChunk = pWholeString->Mid(range.GetStart() - offset, (size_t) len); + + // Replace line break characters with spaces + wxString toRemove = wxRichTextLineBreakChar; + stringChunk.Replace(toRemove, wxT(" ")); + if (textAttr.HasTextEffects() && + (textAttr.GetTextEffects() & (wxTEXT_ATTR_EFFECT_CAPITALS|wxTEXT_ATTR_EFFECT_SMALL_CAPITALS))) + { + stringChunk.MakeUpper(); + } } + else + { + stringWhole = m_text; + if (context.HasVirtualText(this)) + { + if (!context.GetVirtualText(this, stringWhole) || stringWhole.Length() != m_text.Length()) + stringWhole = m_text; + } - // Replace line break characters with spaces - wxString toRemove = wxRichTextLineBreakChar; - str.Replace(toRemove, wxT(" ")); - if (textAttr.HasTextEffects() && (textAttr.GetTextEffects() & (wxTEXT_ATTR_EFFECT_CAPITALS|wxTEXT_ATTR_EFFECT_SMALL_CAPITALS))) - str.MakeUpper(); + // Replace line break characters with spaces + wxString toRemove = wxRichTextLineBreakChar; + stringWhole.Replace(toRemove, wxT(" ")); + if (textAttr.HasTextEffects() && + (textAttr.GetTextEffects() & (wxTEXT_ATTR_EFFECT_CAPITALS|wxTEXT_ATTR_EFFECT_SMALL_CAPITALS))) + { + stringWhole.MakeUpper(); + } - long len = range.GetLength(); - wxString stringChunk = str.Mid(range.GetStart() - offset, (size_t) len); + long len = range.GetLength(); + stringChunk = stringWhole.Mid(range.GetStart() - offset, (size_t) len); + } // Test for the optimized situations where all is selected, or none // is selected. @@ -6903,12 +6939,12 @@ bool wxRichTextPlainText::Draw(wxDC& dc, wxRichTextDrawingContext& context, cons // TODO: new selection code // (a) All selected. - if (selectionRange.GetStart() <= range.GetStart() && selectionRange.GetEnd() >= range.GetEnd()) + if (allSelected) { DrawTabbedString(dc, textAttr, rect, stringChunk, x, y, true); } // (b) None selected. - else if (selectionRange.GetEnd() < range.GetStart() || selectionRange.GetStart() > range.GetEnd()) + else if (noneSelected) { // Draw all unselected DrawTabbedString(dc, textAttr, rect, stringChunk, x, y, false); @@ -6918,6 +6954,8 @@ bool wxRichTextPlainText::Draw(wxDC& dc, wxRichTextDrawingContext& context, cons // (c) Part selected, part not // Let's draw unselected chunk, selected chunk, then unselected chunk. + const wxString& str = stringWhole; + dc.SetBackgroundMode(wxBRUSHSTYLE_TRANSPARENT); // 1. Initial unselected chunk, if any, up until start of selection. @@ -7219,23 +7257,32 @@ bool wxRichTextPlainText::GetRangeSize(const wxRichTextRange& range, wxSize& siz bool haveDescent = false; int startPos = range.GetStart() - GetRange().GetStart(); - long len = range.GetLength(); - wxString str(m_text); - if (context.HasVirtualText(this)) + wxString stringChunk; + { - if (!context.GetVirtualText(this, str) || str.Length() != m_text.Length()) - str = m_text; + // We don't need stringWhole. Only prepare stringChunk. + wxString stringWhole; + const wxString* pWholeString = &m_text; + if (context.HasVirtualText(this)) + { + if (context.GetVirtualText(this, stringWhole) && stringWhole.Length() == m_text.Length()) + pWholeString = &stringWhole; + } + + long len = range.GetLength(); + stringChunk = pWholeString->Mid(startPos, (size_t) len); + + // Replace line break characters with spaces + wxString toRemove = wxRichTextLineBreakChar; + stringChunk.Replace(toRemove, wxT(" ")); + if (textAttr.HasTextEffects() && + (textAttr.GetTextEffects() & (wxTEXT_ATTR_EFFECT_CAPITALS|wxTEXT_ATTR_EFFECT_SMALL_CAPITALS))) + { + stringChunk.MakeUpper(); + } } - wxString toReplace = wxRichTextLineBreakChar; - str.Replace(toReplace, wxT(" ")); - - wxString stringChunk = str.Mid(startPos, (size_t) len); - - if (textAttr.HasTextEffects() && (textAttr.GetTextEffects() & (wxTEXT_ATTR_EFFECT_CAPITALS|wxTEXT_ATTR_EFFECT_SMALL_CAPITALS))) - stringChunk.MakeUpper(); - wxCoord w, h; int width = 0; if (stringChunk.Find(wxT('\t')) != wxNOT_FOUND) From c1250bc0e78e80d054d63b8c223427607088a95a Mon Sep 17 00:00:00 2001 From: Mehmet Soyturk Date: Tue, 12 Oct 2021 16:35:33 +0300 Subject: [PATCH 3/3] Replaced assert by wxASSERT --- src/richtext/richtextbuffer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/richtext/richtextbuffer.cpp b/src/richtext/richtextbuffer.cpp index 6156bb2820..8cb6753e7e 100644 --- a/src/richtext/richtextbuffer.cpp +++ b/src/richtext/richtextbuffer.cpp @@ -6530,7 +6530,7 @@ wxRichTextLine* wxRichTextParagraph::AllocateLine(int pos) { if (pos < (int) m_cachedLines.GetCount()) { - assert(m_cachedLinesVect.size() == m_cachedLines.GetCount()); + wxASSERT(m_cachedLinesVect.size() == m_cachedLines.GetCount()); wxRichTextLine* line = m_cachedLinesVect[pos]; line->Init(this); return line;