From 38ddd614f2329793d7b5a75c4c2fa1e3b5f0fb76 Mon Sep 17 00:00:00 2001 From: =?utf8?q?V=C3=A1clav=20Slav=C3=ADk?= Date: Wed, 9 Feb 2011 19:52:22 +0000 Subject: [PATCH] Calculate ellipsized width exactly. Width calculation using partial extents is just an inaccurate estimate: partial extents have sub-pixel precision and are rounded by GetPartialTextExtents(). Use partial extents to estimate string width and only verify it with GetTextExtent() when it looks good. git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@66873 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- src/common/ctrlcmn.cpp | 248 +++++++++++++++++++++++++---------------- 1 file changed, 155 insertions(+), 93 deletions(-) diff --git a/src/common/ctrlcmn.cpp b/src/common/ctrlcmn.cpp index d08cb255dc..ce76a88625 100755 --- a/src/common/ctrlcmn.cpp +++ b/src/common/ctrlcmn.cpp @@ -236,6 +236,135 @@ wxControlBase::GetCompositeControlsDefaultAttributes(wxWindowVariant WXUNUSED(va #define wxELLIPSE_REPLACEMENT wxS("...") +namespace +{ + +struct EllipsizeCalculator +{ + EllipsizeCalculator(const wxString& s, const wxDC& dc, + int maxFinalWidthPx, int replacementWidthPx) + : + m_initialCharToRemove(0), + m_nCharsToRemove(0), + m_outputNeedsUpdate(true), + m_str(s), + m_dc(dc), + m_maxFinalWidthPx(maxFinalWidthPx), + m_replacementWidthPx(replacementWidthPx) + { + m_isOk = dc.GetPartialTextExtents(s, m_charOffsetsPx); + wxASSERT( m_charOffsetsPx.GetCount() == s.length() ); + } + + bool IsOk() const { return m_isOk; } + + bool EllipsizationNotNeeded() const + { + // NOTE: charOffsetsPx[n] is the width in pixels of the first n characters (with the last one INCLUDED) + // thus charOffsetsPx[len-1] is the total width of the string + return m_charOffsetsPx.Last() <= m_maxFinalWidthPx; + } + + void Init(size_t initialCharToRemove, size_t nCharsToRemove) + { + m_initialCharToRemove = initialCharToRemove; + m_nCharsToRemove = nCharsToRemove; + } + + void RemoveFromEnd() + { + m_nCharsToRemove++; + } + + void RemoveFromStart() + { + m_initialCharToRemove--; + m_nCharsToRemove++; + } + + size_t GetFirstRemoved() const { return m_initialCharToRemove; } + size_t GetLastRemoved() const { return m_initialCharToRemove + m_nCharsToRemove - 1; } + + const wxString& GetEllipsizedText() + { + if ( m_outputNeedsUpdate ) + { + wxASSERT(m_initialCharToRemove <= m_str.length() - 1); // see valid range for initialCharToRemove above + wxASSERT(m_nCharsToRemove >= 1 && m_nCharsToRemove <= m_str.length() - m_initialCharToRemove); // see valid range for nCharsToRemove above + + // erase m_nCharsToRemove characters after m_initialCharToRemove (included); + // e.g. if we have the string "foobar" (len = 6) + // ^ + // \--- m_initialCharToRemove = 2 + // and m_nCharsToRemove = 2, then we get "foar" + m_output = m_str; + m_output.replace(m_initialCharToRemove, m_nCharsToRemove, wxELLIPSE_REPLACEMENT); + } + + return m_output; + } + + bool IsShortEnough() + { + if ( m_nCharsToRemove == m_str.length() ) + return true; // that's the best we could do + + // Width calculation using partial extents is just an inaccurate + // estimate: partial extents have sub-pixel precision and are rounded + // by GetPartialTextExtents(); replacing part of the string with "..." + // may change them too thanks to changes in ligatures, kerning etc. + // + // The correct algorithm would be to call GetTextExtent() in every step + // of ellipsization, but that would be too expensive, especially when + // the difference is just a few pixels. So we use partial extents to + // estimate string width and only verify it with GetTextExtent() when + // it looks good. + + int estimatedWidth = m_replacementWidthPx; // length of "..." + + // length of text before the removed part: + if ( m_initialCharToRemove > 0 ) + estimatedWidth += m_charOffsetsPx[m_initialCharToRemove - 1]; + + // length of text after the removed part: + + if ( GetLastRemoved() < m_str.length() ) + estimatedWidth += m_charOffsetsPx.Last() - m_charOffsetsPx[GetLastRemoved()]; + + if ( estimatedWidth > m_maxFinalWidthPx ) + return false; + + return m_dc.GetTextExtent(GetEllipsizedText()).GetWidth() <= m_maxFinalWidthPx; + } + + // calculation state: + + // REMEMBER: indexes inside the string have a valid range of [0;len-1] if not otherwise constrained + // lengths/counts of characters (e.g. nCharsToRemove) have a + // valid range of [0;len] if not otherwise constrained + // NOTE: since this point we know we have for sure a non-empty string from which we need + // to remove _at least_ one character (thus nCharsToRemove below is constrained to be >= 1) + + // index of first character to erase, valid range is [0;len-1]: + size_t m_initialCharToRemove; + // how many chars do we need to erase? valid range is [0;len-m_initialCharToRemove] + size_t m_nCharsToRemove; + + wxString m_output; + bool m_outputNeedsUpdate; + + // inputs: + wxString m_str; + const wxDC& m_dc; + int m_maxFinalWidthPx; + int m_replacementWidthPx; + wxArrayInt m_charOffsetsPx; + + bool m_isOk; +}; + +} // anonymous namespace + /* static and protected */ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxDC& dc, wxEllipsizeMode mode, int maxFinalWidthPx, @@ -253,43 +382,28 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD if (maxFinalWidthPx <= 0) return wxEmptyString; - wxArrayInt charOffsetsPx; size_t len = curLine.length(); - if (len <= 1 || - !dc.GetPartialTextExtents(curLine, charOffsetsPx)) + if (len <= 1 ) return curLine; - wxASSERT(charOffsetsPx.GetCount() == len); - - // NOTE: charOffsetsPx[n] is the width in pixels of the first n characters (with the last one INCLUDED) - // thus charOffsetsPx[len-1] is the total width of the string - size_t totalWidthPx = charOffsetsPx.Last(); - if ( totalWidthPx <= (size_t)maxFinalWidthPx ) - return curLine; // we don't need to do any ellipsization! + EllipsizeCalculator calc(curLine, dc, maxFinalWidthPx, replacementWidthPx); - int excessPx = wxMin(totalWidthPx - maxFinalWidthPx + - replacementWidthPx, - totalWidthPx); - wxASSERT(excessPx>0); // excessPx should be in the [1;totalWidthPx] range - - // REMEMBER: indexes inside the string have a valid range of [0;len-1] if not otherwise constrained - // lengths/counts of characters (e.g. nCharsToRemove) have a valid range of [0;len] if not otherwise constrained - // NOTE: since this point we know we have for sure a non-empty string from which we need - // to remove _at least_ one character (thus nCharsToRemove below is constrained to be >= 1) + if ( !calc.IsOk() ) + return curLine; - size_t initialCharToRemove, // index of first character to erase, valid range is [0;len-1] - nCharsToRemove; // how many chars do we need to erase? valid range is [1;len-initialCharToRemove] + if ( calc.EllipsizationNotNeeded() ) + return curLine; // let's compute the range of characters to remove depending on the ellipsization mode: switch (mode) { case wxELLIPSIZE_START: - initialCharToRemove = 0; - for ( nCharsToRemove = 1; - nCharsToRemove < len && charOffsetsPx[nCharsToRemove-1] < excessPx; - nCharsToRemove++ ) - ; - break; + { + calc.Init(0, 1); + while ( !calc.IsShortEnough() ) + calc.RemoveFromEnd(); + break; + } case wxELLIPSIZE_MIDDLE: { @@ -301,15 +415,15 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD // - the second one to remove, valid range [initialCharToRemove;endCharToRemove] // - the third one to preserve, valid range [endCharToRemove+1;len-1] or the empty range if endCharToRemove==len-1 // NOTE: empty range != range [0;0] since the range [0;0] contains 1 character (the zero-th one)! - initialCharToRemove = len/2; // index of the last character to remove; valid range is [0;len-1] - size_t endCharToRemove = initialCharToRemove - 1; // initial removal range is empty - int removedPx = 0; + calc.Init(len/2, 0); + bool removeFromStart = true; - for ( ; removedPx < excessPx; ) + + while ( !calc.IsShortEnough() ) { - const bool canRemoveFromStart = initialCharToRemove > 0; - const bool canRemoveFromEnd = endCharToRemove < len - 1; + const bool canRemoveFromStart = calc.GetFirstRemoved() > 0; + const bool canRemoveFromEnd = calc.GetLastRemoved() < len - 1; if ( !canRemoveFromStart && !canRemoveFromEnd ) { @@ -326,59 +440,20 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD removeFromStart = true; if ( removeFromStart ) - { - // try to remove the last character of the first part of the string - - // width of the (initialCharToRemove-1)-th character - int widthPx; - if (initialCharToRemove >= 2) - widthPx = charOffsetsPx[initialCharToRemove-1] - charOffsetsPx[initialCharToRemove-2]; - else - widthPx = charOffsetsPx[initialCharToRemove-1]; - // the (initialCharToRemove-1)-th character is the first char of the string - - wxASSERT(widthPx >= 0); // widthPx is zero for e.g. tab characters - - // mark the (initialCharToRemove-1)-th character as removable - initialCharToRemove--; - removedPx += widthPx; - - continue; // don't remove anything else - } - else // !removeFromStart - { - // try to remove the first character of the last part of the string - - // width of the (endCharToRemove+1)-th character - int widthPx = charOffsetsPx[endCharToRemove+1] - - charOffsetsPx[endCharToRemove]; - - wxASSERT(widthPx >= 0); // widthPx is zero for e.g. tab characters - - // mark the (endCharToRemove+1)-th character as removable - endCharToRemove++; - removedPx += widthPx; - - continue; // don't remove anything else - } + calc.RemoveFromStart(); + else + calc.RemoveFromEnd(); } - - nCharsToRemove = endCharToRemove - initialCharToRemove + 1; } break; case wxELLIPSIZE_END: { - int maxWidthPx = totalWidthPx - excessPx; - - // go backward from the end of the string toward the start - for ( initialCharToRemove = len-1; - initialCharToRemove > 0 && charOffsetsPx[initialCharToRemove-1] > maxWidthPx; - initialCharToRemove-- ) - ; - nCharsToRemove = len - initialCharToRemove; + calc.Init(len - 1, 1); + while ( !calc.IsShortEnough() ) + calc.RemoveFromStart(); + break; } - break; case wxELLIPSIZE_NONE: default: @@ -386,20 +461,7 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD return curLine; } - wxASSERT(initialCharToRemove <= len-1); // see valid range for initialCharToRemove above - wxASSERT(nCharsToRemove >= 1 && nCharsToRemove <= len-initialCharToRemove); // see valid range for nCharsToRemove above - - // erase nCharsToRemove characters after initialCharToRemove (included); - // e.g. if we have the string "foobar" (len = 6) - // ^ - // \--- initialCharToRemove = 2 - // and nCharsToRemove = 2, then we get "foar" - wxString ret(curLine); - ret.erase(initialCharToRemove, nCharsToRemove); - - ret.insert(initialCharToRemove, wxELLIPSE_REPLACEMENT); - - return ret; + return calc.GetEllipsizedText(); } /* static */ -- 2.45.2