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:
@@ -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 */
|
||||||
|
Reference in New Issue
Block a user