tdf#156377: Improve cursor traversal in UI widgets
Re-implement GetCaretPositions() on top of GetCharWidths() so it can
benefit from our code that handles cursor insertion in middle of
ligatures.
Change-Id: I2b76b3b0125f2454f78cb6779d88617dc29386fe
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/154660
Tested-by: Jenkins
Reviewed-by: خالد حسني <khaled@libreoffice.org>
diff --git a/include/vcl/outdev.hxx b/include/vcl/outdev.hxx
index 7e0a17f..f3d2e91 100644
--- a/include/vcl/outdev.hxx
+++ b/include/vcl/outdev.hxx
@@ -1047,7 +1047,7 @@ public:
vcl::text::TextLayoutCache const* = nullptr,
SalLayoutGlyphs const*const pLayoutCache = nullptr) const;
void GetCaretPositions( const OUString&, sal_Int32* pCaretXArray,
void GetCaretPositions( const OUString&, KernArray& rCaretXArray,
sal_Int32 nIndex, sal_Int32 nLen,
const SalLayoutGlyphs* pGlyphs = nullptr ) const;
void DrawStretchText( const Point& rStartPt, sal_Int32 nWidth,
diff --git a/include/vcl/vcllayout.hxx b/include/vcl/vcllayout.hxx
index dda07f1..7c7a179 100644
--- a/include/vcl/vcllayout.hxx
+++ b/include/vcl/vcllayout.hxx
@@ -96,7 +96,7 @@ public:
virtual sal_Int32 GetTextBreak(double nMaxWidth, double nCharExtra, int nFactor) const = 0;
virtual double FillDXArray( std::vector<double>* pDXArray, const OUString& rStr ) const = 0;
virtual double GetTextWidth() const { return FillDXArray( nullptr, {} ); }
virtual void GetCaretPositions( int nArraySize, sal_Int32* pCaretXArray ) const = 0;
virtual void GetCaretPositions( std::vector<double>& rCaretPositions, const OUString& rStr ) const = 0;
virtual bool IsKashidaPosValid ( int /*nCharPos*/, int /*nNextCharPos*/ ) const = 0; // i60594
// methods using glyph indexing
diff --git a/vcl/inc/sallayout.hxx b/vcl/inc/sallayout.hxx
index f96eb7d..dbe3000 100644
--- a/vcl/inc/sallayout.hxx
+++ b/vcl/inc/sallayout.hxx
@@ -64,7 +64,7 @@ public:
sal_Int32 GetTextBreak(double nMaxWidth, double nCharExtra, int nFactor) const override;
double GetTextWidth() const final override;
double FillDXArray(std::vector<double>* pDXArray, const OUString& rStr) const override;
void GetCaretPositions(int nArraySize, sal_Int32* pCaretXArray) const override;
void GetCaretPositions(std::vector<double>& rCaretPositions, const OUString& rStr) const override;
bool GetNextGlyph(const GlyphItem** pGlyph, basegfx::B2DPoint& rPos, int& nStart,
const LogicalFontInstance** ppGlyphFont = nullptr) const override;
bool GetOutline(basegfx::B2DPolyPolygonVector&) const override;
@@ -120,7 +120,7 @@ public:
double GetTextWidth() const final override;
double FillDXArray(std::vector<double>* pDXArray, const OUString& rStr) const final override;
sal_Int32 GetTextBreak(double nMaxWidth, double nCharExtra, int nFactor) const final override;
void GetCaretPositions(int nArraySize, sal_Int32* pCaretXArray) const final override;
void GetCaretPositions(std::vector<double>& rCaretPositions, const OUString& rStr) const override;
// used by display layers
LogicalFontInstance& GetFont() const
diff --git a/vcl/source/control/edit.cxx b/vcl/source/control/edit.cxx
index 10c6841..0db27e8 100644
--- a/vcl/source/control/edit.cxx
+++ b/vcl/source/control/edit.cxx
@@ -473,20 +473,9 @@ void Edit::ImplRepaint(vcl::RenderContext& rRenderContext, const tools::Rectangl
const OUString aText = ImplGetText();
const sal_Int32 nLen = aText.getLength();
sal_Int32 nDXBuffer[256];
std::unique_ptr<sal_Int32[]> pDXBuffer;
sal_Int32* pDX = nDXBuffer;
KernArray aDX;
if (nLen)
{
if (o3tl::make_unsigned(2 * nLen) > SAL_N_ELEMENTS(nDXBuffer))
{
pDXBuffer.reset(new sal_Int32[2 * (nLen + 1)]);
pDX = pDXBuffer.get();
}
GetOutDev()->GetCaretPositions(aText, pDX, 0, nLen);
}
GetOutDev()->GetCaretPositions(aText, aDX, 0, nLen);
tools::Long nTH = GetTextHeight();
Point aPos(mnXOffset, ImplGetTextYPosition());
@@ -548,8 +537,8 @@ void Edit::ImplRepaint(vcl::RenderContext& rRenderContext, const tools::Rectangl
for(sal_Int32 i = 0; i < nLen; ++i)
{
tools::Rectangle aRect(aPos, Size(10, nTH));
aRect.SetLeft( pDX[2 * i] + mnXOffset + ImplGetExtraXOffset() );
aRect.SetRight( pDX[2 * i + 1] + mnXOffset + ImplGetExtraXOffset() );
aRect.SetLeft( aDX[2 * i] + mnXOffset + ImplGetExtraXOffset() );
aRect.SetRight( aDX[2 * i + 1] + mnXOffset + ImplGetExtraXOffset() );
aRect.Normalize();
bool bHighlight = false;
if (i >= aTmpSel.Min() && i < aTmpSel.Max())
@@ -626,8 +615,8 @@ void Edit::ImplRepaint(vcl::RenderContext& rRenderContext, const tools::Rectangl
while (nIndex < mpIMEInfos->nLen && mpIMEInfos->pAttribs[nIndex] == nAttr) // #112631# check nIndex before using it
{
tools::Rectangle aRect( aPos, Size( 10, nTH ) );
aRect.SetLeft( pDX[2 * (nIndex + mpIMEInfos->nPos)] + mnXOffset + ImplGetExtraXOffset() );
aRect.SetRight( pDX[2 * (nIndex + mpIMEInfos->nPos) + 1] + mnXOffset + ImplGetExtraXOffset() );
aRect.SetLeft( aDX[2 * (nIndex + mpIMEInfos->nPos)] + mnXOffset + ImplGetExtraXOffset() );
aRect.SetRight( aDX[2 * (nIndex + mpIMEInfos->nPos) + 1] + mnXOffset + ImplGetExtraXOffset() );
aRect.Normalize();
aClip.Union(aRect);
nIndex++;
@@ -1072,24 +1061,15 @@ void Edit::ImplShowCursor( bool bOnlyIfVisible )
tools::Long nTextPos = 0;
sal_Int32 nDXBuffer[256];
std::unique_ptr<sal_Int32[]> pDXBuffer;
sal_Int32* pDX = nDXBuffer;
if( !aText.isEmpty() )
{
if( o3tl::make_unsigned(2*aText.getLength()) > SAL_N_ELEMENTS(nDXBuffer) )
{
pDXBuffer.reset(new sal_Int32[2*(aText.getLength()+1)]);
pDX = pDXBuffer.get();
}
GetOutDev()->GetCaretPositions( aText, pDX, 0, aText.getLength() );
KernArray aDX;
GetOutDev()->GetCaretPositions(aText, aDX, 0, aText.getLength());
if( maSelection.Max() < aText.getLength() )
nTextPos = pDX[ 2*maSelection.Max() ];
nTextPos = aDX[ 2*maSelection.Max() ];
else
nTextPos = pDX[ 2*aText.getLength()-1 ];
nTextPos = aDX[ 2*aText.getLength()-1 ];
}
tools::Long nCursorWidth = 0;
@@ -1196,31 +1176,26 @@ sal_Int32 Edit::ImplGetCharPos( const Point& rWindowPos ) const
sal_Int32 nIndex = EDIT_NOLIMIT;
OUString aText = ImplGetText();
sal_Int32 nDXBuffer[256];
std::unique_ptr<sal_Int32[]> pDXBuffer;
sal_Int32* pDX = nDXBuffer;
if( o3tl::make_unsigned(2*aText.getLength()) > SAL_N_ELEMENTS(nDXBuffer) )
{
pDXBuffer.reset(new sal_Int32[2*(aText.getLength()+1)]);
pDX = pDXBuffer.get();
}
if (aText.isEmpty())
return nIndex;
GetOutDev()->GetCaretPositions( aText, pDX, 0, aText.getLength() );
KernArray aDX;
GetOutDev()->GetCaretPositions(aText, aDX, 0, aText.getLength());
tools::Long nX = rWindowPos.X() - mnXOffset - ImplGetExtraXOffset();
for (sal_Int32 i = 0; i < aText.getLength(); aText.iterateCodePoints(&i))
{
if( (pDX[2*i] >= nX && pDX[2*i+1] <= nX) ||
(pDX[2*i+1] >= nX && pDX[2*i] <= nX))
if( (aDX[2*i] >= nX && aDX[2*i+1] <= nX) ||
(aDX[2*i+1] >= nX && aDX[2*i] <= nX))
{
nIndex = i;
if( pDX[2*i] < pDX[2*i+1] )
if( aDX[2*i] < aDX[2*i+1] )
{
if( nX > (pDX[2*i]+pDX[2*i+1])/2 )
if( nX > (aDX[2*i]+aDX[2*i+1])/2 )
aText.iterateCodePoints(&nIndex);
}
else
{
if( nX < (pDX[2*i]+pDX[2*i+1])/2 )
if( nX < (aDX[2*i]+aDX[2*i+1])/2 )
aText.iterateCodePoints(&nIndex);
}
break;
@@ -1230,7 +1205,7 @@ sal_Int32 Edit::ImplGetCharPos( const Point& rWindowPos ) const
{
nIndex = 0;
sal_Int32 nFinalIndex = 0;
tools::Long nDiff = std::abs( pDX[0]-nX );
tools::Long nDiff = std::abs( aDX[0]-nX );
sal_Int32 i = 0;
if (!aText.isEmpty())
{
@@ -1238,7 +1213,7 @@ sal_Int32 Edit::ImplGetCharPos( const Point& rWindowPos ) const
}
while (i < aText.getLength())
{
tools::Long nNewDiff = std::abs( pDX[2*i]-nX );
tools::Long nNewDiff = std::abs( aDX[2*i]-nX );
if( nNewDiff < nDiff )
{
@@ -1250,7 +1225,7 @@ sal_Int32 Edit::ImplGetCharPos( const Point& rWindowPos ) const
aText.iterateCodePoints(&i);
}
if (nIndex == nFinalIndex && std::abs( pDX[2*nIndex+1] - nX ) < nDiff)
if (nIndex == nFinalIndex && std::abs( aDX[2*nIndex+1] - nX ) < nDiff)
nIndex = EDIT_NOLIMIT;
}
@@ -2162,9 +2137,8 @@ void Edit::Command( const CommandEvent& rCEvt )
if (mpIMEInfos && mpIMEInfos->nLen > 0)
{
OUString aText = ImplGetText();
std::vector<sal_Int32> aDX(2*(aText.getLength()+1));
GetOutDev()->GetCaretPositions( aText, aDX.data(), 0, aText.getLength() );
KernArray aDX;
GetOutDev()->GetCaretPositions(aText, aDX, 0, aText.getLength());
tools::Long nTH = GetTextHeight();
Point aPos( mnXOffset, ImplGetTextYPosition() );
diff --git a/vcl/source/gdi/sallayout.cxx b/vcl/source/gdi/sallayout.cxx
index 65bf46f..af28112 100644
--- a/vcl/source/gdi/sallayout.cxx
+++ b/vcl/source/gdi/sallayout.cxx
@@ -439,33 +439,57 @@ void GenericSalLayout::ApplyAsianKerning(std::u16string_view rStr)
}
}
void GenericSalLayout::GetCaretPositions( int nMaxIndex, sal_Int32* pCaretXArray ) const
void GenericSalLayout::GetCaretPositions(std::vector<double>& rCaretPositions,
const OUString& rStr) const
{
// initialize result array
for (int i = 0; i < nMaxIndex; ++i)
pCaretXArray[i] = -1;
const int nCaretPositions = (mnEndCharPos - mnMinCharPos) * 2;
rCaretPositions.clear();
rCaretPositions.resize(nCaretPositions, -1);
if (m_GlyphItems.empty())
return;
std::vector<double> aCharWidths;
GetCharWidths(aCharWidths, rStr);
// calculate caret positions using glyph array
for (auto const& aGlyphItem : m_GlyphItems)
{
tools::Long nXPos = aGlyphItem.linearPos().getX();
tools::Long nXRight = nXPos + aGlyphItem.origWidth();
int n = aGlyphItem.charPos();
int nCurrIdx = 2 * (n - mnMinCharPos);
// tdf#86399 if this is not the start of a cluster, don't overwrite the caret bounds of the cluster start
if (aGlyphItem.IsInCluster() && pCaretXArray[nCurrIdx] != -1)
continue;
if (!aGlyphItem.IsRTLGlyph() )
auto nCurrX = aGlyphItem.linearPos().getX() - aGlyphItem.xOffset();
auto nCharStart = aGlyphItem.charPos();
auto nCharEnd = nCharStart + aGlyphItem.charCount() - 1;
if (!aGlyphItem.IsRTLGlyph())
{
// normal positions for LTR case
pCaretXArray[ nCurrIdx ] = nXPos;
pCaretXArray[ nCurrIdx+1 ] = nXRight;
// unchanged positions for LTR case
for (int i = nCharStart; i <= nCharEnd; i++)
{
int n = i - mnMinCharPos;
int nCurrIdx = 2 * n;
auto nLeft = nCurrX;
nCurrX += aCharWidths[n];
auto nRight = nCurrX;
rCaretPositions[nCurrIdx] = nLeft;
rCaretPositions[nCurrIdx + 1] = nRight;
}
}
else
{
// reverse positions for RTL case
pCaretXArray[ nCurrIdx ] = nXRight;
pCaretXArray[ nCurrIdx+1 ] = nXPos;
for (int i = nCharEnd; i >= nCharStart; i--)
{
int n = i - mnMinCharPos;
int nCurrIdx = 2 * n;
auto nRight = nCurrX;
nCurrX += aCharWidths[n];
auto nLeft = nCurrX;
rCaretPositions[nCurrIdx] = nLeft;
rCaretPositions[nCurrIdx + 1] = nRight;
}
}
}
}
@@ -1054,21 +1078,29 @@ double MultiSalLayout::FillDXArray( std::vector<double>* pCharWidths, const OUSt
return GetTextWidth();
}
void MultiSalLayout::GetCaretPositions( int nMaxIndex, sal_Int32* pCaretXArray ) const
void MultiSalLayout::GetCaretPositions(std::vector<double>& rCaretPositions,
const OUString& rStr) const
{
SalLayout& rLayout = *mpLayouts[ 0 ];
rLayout.GetCaretPositions( nMaxIndex, pCaretXArray );
// prepare merging of fallback levels
std::vector<double> aTempPos;
const int nCaretPositions = (mnEndCharPos - mnMinCharPos) * 2;
rCaretPositions.clear();
rCaretPositions.resize(nCaretPositions, -1);
if( mnLevel <= 1 )
return;
std::unique_ptr<sal_Int32[]> const pTempPos(new sal_Int32[nMaxIndex]);
for( int n = 1; n < mnLevel; ++n )
for (int n = mnLevel; --n >= 0;)
{
mpLayouts[ n ]->GetCaretPositions( nMaxIndex, pTempPos.get() );
for( int i = 0; i < nMaxIndex; ++i )
if( pTempPos[i] >= 0 )
pCaretXArray[i] = pTempPos[i];
// query every fallback level
mpLayouts[n]->GetCaretPositions(aTempPos, rStr);
// calculate virtual char widths using most probable fallback layout
for (int i = 0; i < nCaretPositions; ++i)
{
// one char cannot be resolved from different fallbacks
if (rCaretPositions[i] != -1)
continue;
if (aTempPos[i] >= 0)
rCaretPositions[i] = aTempPos[i];
}
}
}
diff --git a/vcl/source/outdev/text.cxx b/vcl/source/outdev/text.cxx
index aaa165b..435110a 100644
--- a/vcl/source/outdev/text.cxx
+++ b/vcl/source/outdev/text.cxx
@@ -1035,7 +1035,7 @@ tools::Long OutputDevice::GetTextArray( const OUString& rStr, KernArray* pKernAr
return basegfx::fround(nWidth);
}
void OutputDevice::GetCaretPositions( const OUString& rStr, sal_Int32* pCaretXArray,
void OutputDevice::GetCaretPositions( const OUString& rStr, KernArray& rCaretXArray,
sal_Int32 nIndex, sal_Int32 nLen,
const SalLayoutGlyphs* pGlyphs ) const
{
@@ -1045,45 +1045,59 @@ void OutputDevice::GetCaretPositions( const OUString& rStr, sal_Int32* pCaretXAr
if( nIndex+nLen >= rStr.getLength() )
nLen = rStr.getLength() - nIndex;
// layout complex text
sal_Int32 nCaretPos = nLen * 2;
std::vector<sal_Int32>& rCaretPos = rCaretXArray.get_subunit_array();
rCaretPos.resize(nCaretPos);
// do layout
std::unique_ptr<SalLayout> pSalLayout = ImplLayout(rStr, nIndex, nLen, Point(0, 0), 0, {}, {},
eDefaultLayout, nullptr, pGlyphs);
if( !pSalLayout )
{
std::fill(pCaretXArray, pCaretXArray + nLen * 2, -1);
std::fill(rCaretPos.begin(), rCaretPos.end(), -1);
return;
}
pSalLayout->GetCaretPositions( 2*nLen, pCaretXArray );
double nWidth = pSalLayout->GetTextWidth();
std::vector<double> aCaretPixelPos;
pSalLayout->GetCaretPositions(aCaretPixelPos, rStr);
// fixup unknown caret positions
int i;
for( i = 0; i < 2 * nLen; ++i )
if( pCaretXArray[ i ] >= 0 )
for (i = 0; i < nCaretPos; ++i)
if (aCaretPixelPos[i] >= 0)
break;
tools::Long nXPos = (i < 2 * nLen) ? pCaretXArray[i] : -1;
for( i = 0; i < 2 * nLen; ++i )
tools::Long nXPos = (i < nCaretPos) ? aCaretPixelPos[i] : -1;
for (i = 0; i < nCaretPos; ++i)
{
if( pCaretXArray[ i ] >= 0 )
nXPos = pCaretXArray[ i ];
if (aCaretPixelPos[i] >= 0)
nXPos = aCaretPixelPos[i];
else
pCaretXArray[ i ] = nXPos;
aCaretPixelPos[i] = nXPos;
}
// handle window mirroring
if( IsRTLEnabled() )
{
for( i = 0; i < 2 * nLen; ++i )
pCaretXArray[i] = nWidth - pCaretXArray[i] - 1;
double nWidth = pSalLayout->GetTextWidth();
for (i = 0; i < nCaretPos; ++i)
aCaretPixelPos[i] = nWidth - aCaretPixelPos[i] - 1;
}
int nSubPixelFactor = rCaretXArray.get_factor();
// convert from font units to logical units
if( mbMap )
{
for( i = 0; i < 2*nLen; ++i )
pCaretXArray[i] = ImplDevicePixelToLogicWidth( pCaretXArray[i] );
for (i = 0; i < nCaretPos; ++i)
aCaretPixelPos[i] = ImplDevicePixelToLogicWidth(aCaretPixelPos[i] * nSubPixelFactor);
}
else if (nSubPixelFactor)
{
for (i = 0; i < nCaretPos; ++i)
aCaretPixelPos[i] *= nSubPixelFactor;
}
for (i = 0; i < nCaretPos; ++i)
rCaretPos[i] = basegfx::fround(aCaretPixelPos[i]);
}
void OutputDevice::DrawStretchText( const Point& rStartPt, sal_Int32 nWidth,