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("...")
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);
EllipsizeCalculator calc(curLine, dc, maxFinalWidthPx, replacementWidthPx);
// 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!
if ( !calc.IsOk() )
return curLine;
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)
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 */