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
This commit is contained in:
Václav Slavík
2011-02-09 19:52:22 +00:00
parent 508fc76d0c
commit 38ddd614f2

View File

@@ -236,6 +236,135 @@ wxControlBase::GetCompositeControlsDefaultAttributes(wxWindowVariant WXUNUSED(va
#define wxELLIPSE_REPLACEMENT wxS("...") #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 */ /* static and protected */
wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxDC& dc, wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxDC& dc,
wxEllipsizeMode mode, int maxFinalWidthPx, wxEllipsizeMode mode, int maxFinalWidthPx,
@@ -253,43 +382,28 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD
if (maxFinalWidthPx <= 0) if (maxFinalWidthPx <= 0)
return wxEmptyString; return wxEmptyString;
wxArrayInt charOffsetsPx;
size_t len = curLine.length(); size_t len = curLine.length();
if (len <= 1 || if (len <= 1 )
!dc.GetPartialTextExtents(curLine, charOffsetsPx))
return curLine; return curLine;
wxASSERT(charOffsetsPx.GetCount() == len); EllipsizeCalculator calc(curLine, dc, maxFinalWidthPx, replacementWidthPx);
// NOTE: charOffsetsPx[n] is the width in pixels of the first n characters (with the last one INCLUDED) if ( !calc.IsOk() )
// thus charOffsetsPx[len-1] is the total width of the string return curLine;
size_t totalWidthPx = charOffsetsPx.Last();
if ( totalWidthPx <= (size_t)maxFinalWidthPx )
return curLine; // we don't need to do any ellipsization!
int excessPx = wxMin(totalWidthPx - maxFinalWidthPx + if ( calc.EllipsizationNotNeeded() )
replacementWidthPx, return curLine;
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)
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]
// let's compute the range of characters to remove depending on the ellipsization mode: // let's compute the range of characters to remove depending on the ellipsization mode:
switch (mode) switch (mode)
{ {
case wxELLIPSIZE_START: case wxELLIPSIZE_START:
initialCharToRemove = 0; {
for ( nCharsToRemove = 1; calc.Init(0, 1);
nCharsToRemove < len && charOffsetsPx[nCharsToRemove-1] < excessPx; while ( !calc.IsShortEnough() )
nCharsToRemove++ ) calc.RemoveFromEnd();
;
break; break;
}
case wxELLIPSIZE_MIDDLE: 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 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 // - 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)! // 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; bool removeFromStart = true;
for ( ; removedPx < excessPx; )
while ( !calc.IsShortEnough() )
{ {
const bool canRemoveFromStart = initialCharToRemove > 0; const bool canRemoveFromStart = calc.GetFirstRemoved() > 0;
const bool canRemoveFromEnd = endCharToRemove < len - 1; const bool canRemoveFromEnd = calc.GetLastRemoved() < len - 1;
if ( !canRemoveFromStart && !canRemoveFromEnd ) if ( !canRemoveFromStart && !canRemoveFromEnd )
{ {
@@ -326,59 +440,20 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD
removeFromStart = true; removeFromStart = true;
if ( removeFromStart ) if ( removeFromStart )
{ calc.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 else
widthPx = charOffsetsPx[initialCharToRemove-1]; calc.RemoveFromEnd();
// 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
}
}
nCharsToRemove = endCharToRemove - initialCharToRemove + 1;
} }
break; break;
case wxELLIPSIZE_END: case wxELLIPSIZE_END:
{ {
int maxWidthPx = totalWidthPx - excessPx; calc.Init(len - 1, 1);
while ( !calc.IsShortEnough() )
// go backward from the end of the string toward the start calc.RemoveFromStart();
for ( initialCharToRemove = len-1;
initialCharToRemove > 0 && charOffsetsPx[initialCharToRemove-1] > maxWidthPx;
initialCharToRemove-- )
;
nCharsToRemove = len - initialCharToRemove;
}
break; break;
}
case wxELLIPSIZE_NONE: case wxELLIPSIZE_NONE:
default: default:
@@ -386,20 +461,7 @@ wxString wxControlBase::DoEllipsizeSingleLine(const wxString& curLine, const wxD
return curLine; return curLine;
} }
wxASSERT(initialCharToRemove <= len-1); // see valid range for initialCharToRemove above return calc.GetEllipsizedText();
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;
} }
/* static */ /* static */