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,