tdf#30731: Improve caret travelling in Writer

Previously, when measuring caret position, Writer would measure the
width of the substring before the caret (i.e. layout it independent of
the text after the caret and measure its width).

This is incorrect, though. It assumes cutting the string laying it out
would result in the same width as when laid out as part of a bigger
string, which is invalid assumption when e.g. cutting inside a ligature
or between letters that have different shapes when next to each other,
etc.

This appears to work when the width of the substring laid out alone is
close enough to its width when laid out with the full text. But in cases
where is widths are largely different, like the extreme case in the bug
report, the caret will be jumping around as it is positioned based on
the unligated glyphs not the ligated, rendered glyphs.

This change introduces a special mode of measuring text width for caret
positioning, that will layout the whole string that return the width of
the requested substring.

Fields and small caps text are trickier to handle, so old behaviour is
retained for them. Now one will probably notice but if they do, it can
be dealt with then.

This also tries to be conservative and keep other pleases using the
existing behaviour which might be desirable (e.g. when measuring text
width for line breaking, we want the unligated width), but there might
be other places that should use the new behaviour.

To handle caret inside ligatures, the grapheme clusters in the ligature
are counted and the width of the whole ligature is distributed on them
evenly. A further improvement would be using HarfBuzz API to get
ligature caret positions for fonts that provide them, which helps when
the ligature components have different widths.

Change-Id: I02062e2e2e1b1a35c8f84307c0a8f5d743059ab5
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/138889
Tested-by: Jenkins
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
diff --git a/editeng/source/items/svxfont.cxx b/editeng/source/items/svxfont.cxx
index 868b830..65191ca 100644
--- a/editeng/source/items/svxfont.cxx
+++ b/editeng/source/items/svxfont.cxx
@@ -37,7 +37,7 @@ static tools::Long GetTextArray( const OutputDevice* pOut, const OUString& rStr,

{
    const SalLayoutGlyphs* layoutGlyphs = SalLayoutGlyphsCache::self()->GetLayoutGlyphs(pOut, rStr, nIndex, nLen);
    return pOut->GetTextArray( rStr, pDXAry, nIndex, nLen, nullptr, layoutGlyphs);
    return pOut->GetTextArray( rStr, pDXAry, nIndex, nLen, false, nullptr, layoutGlyphs);
}

SvxFont::SvxFont()
diff --git a/include/vcl/outdev.hxx b/include/vcl/outdev.hxx
index 27e7650..1162ef3 100644
--- a/include/vcl/outdev.hxx
+++ b/include/vcl/outdev.hxx
@@ -1043,7 +1043,7 @@ public:
                                               SalLayoutFlags flags = SalLayoutFlags::NONE,
                                               const SalLayoutGlyphs* pLayoutCache = nullptr);
    tools::Long                        GetTextArray( const OUString& rStr, std::vector<sal_Int32>* pDXAry,
                                              sal_Int32 nIndex = 0, sal_Int32 nLen = -1,
                                              sal_Int32 nIndex = 0, sal_Int32 nLen = -1, bool bCaret = false,
                                              vcl::text::TextLayoutCache const* = nullptr,
                                              SalLayoutGlyphs const*const pLayoutCache = nullptr) const;

diff --git a/include/vcl/vcllayout.hxx b/include/vcl/vcllayout.hxx
index e63d365..cb88760 100644
--- a/include/vcl/vcllayout.hxx
+++ b/include/vcl/vcllayout.hxx
@@ -20,6 +20,7 @@
#pragma once

#include <basegfx/polygon/b2dpolypolygon.hxx>
#include <i18nlangtag/languagetag.hxx>
#include <tools/gen.hxx>
#include <tools/degree.hxx>

@@ -94,8 +95,8 @@ public:

    // methods using string indexing
    virtual sal_Int32 GetTextBreak(DeviceCoordinate nMaxWidth, DeviceCoordinate nCharExtra, int nFactor) const = 0;
    virtual DeviceCoordinate FillDXArray( std::vector<DeviceCoordinate>* pDXArray ) const = 0;
    virtual DeviceCoordinate GetTextWidth() const { return FillDXArray( nullptr ); }
    virtual DeviceCoordinate FillDXArray( std::vector<DeviceCoordinate>* pDXArray, const OUString& rStr ) const = 0;
    virtual DeviceCoordinate GetTextWidth() const { return FillDXArray( nullptr, {} ); }
    virtual void    GetCaretPositions( int nArraySize, sal_Int32* pCaretXArray ) const = 0;
    virtual bool    IsKashidaPosValid ( int /*nCharPos*/, int /*nNextCharPos*/ ) const = 0; // i60594

@@ -119,6 +120,7 @@ private:
protected:
    int             mnMinCharPos;
    int             mnEndCharPos;
    LanguageTag     maLanguageTag;

    int             mnUnitsPerPixel;
    Degree10        mnOrientation;
diff --git a/osx/soffice.xcodeproj/project.pbxproj b/osx/soffice.xcodeproj/project.pbxproj
index 7c201c1..4a2038c 100644
--- a/osx/soffice.xcodeproj/project.pbxproj
+++ b/osx/soffice.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
	objects = {

/* Begin PBXFileReference section */
		29FD821128BC548D00159078 /* text */ = {isa = PBXFileReference; lastKnownFileType = folder; name = text; path = ../sw/source/core/text; sourceTree = "<group>"; };
		29FD821228BC54AB00159078 /* txtnode */ = {isa = PBXFileReference; lastKnownFileType = folder; name = txtnode; path = ../sw/source/core/txtnode; sourceTree = "<group>"; };
		456E58CF277CB9C700FA12D2 /* unoshap2.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = unoshap2.cxx; path = ../svx/source/unodraw/unoshap2.cxx; sourceTree = "<group>"; };
		456E58D1277CC33E00FA12D2 /* unopage.cxx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = unopage.cxx; path = ../svx/source/unodraw/unopage.cxx; sourceTree = "<group>"; };
		BE017B8725AF2ABE00244ED8 /* autostyl.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = autostyl.cxx; path = ../sc/source/ui/docshell/autostyl.cxx; sourceTree = "<group>"; };
@@ -123,7 +125,6 @@
		BE017BF825AF568900244ED8 /* solveroptions.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = solveroptions.cxx; path = ../sc/source/ui/miscdlgs/solveroptions.cxx; sourceTree = "<group>"; };
		BE017BF925AF568900244ED8 /* mtrindlg.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = mtrindlg.cxx; path = ../sc/source/ui/miscdlgs/mtrindlg.cxx; sourceTree = "<group>"; };
		BE017BFA25AF568900244ED8 /* linkarea.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = linkarea.cxx; path = ../sc/source/ui/miscdlgs/linkarea.cxx; sourceTree = "<group>"; };
		BE017BFB25AF568900244ED8 /* shtabdlg.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = shtabdlg.cxx; path = ../sc/source/ui/miscdlgs/shtabdlg.cxx; sourceTree = "<group>"; };
		BE017BFB25AF568900244ED8 /* gototabdlg.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = gototabdlg.cxx; path = ../sc/source/ui/miscdlgs/gototabdlg.cxx; sourceTree = "<group>"; };
		BE017BFC25AF568900244ED8 /* inscodlg.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = inscodlg.cxx; path = ../sc/source/ui/miscdlgs/inscodlg.cxx; sourceTree = "<group>"; };
		BE017BFD25AF568A00244ED8 /* crdlg.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = crdlg.cxx; path = ../sc/source/ui/miscdlgs/crdlg.cxx; sourceTree = "<group>"; };
@@ -1281,7 +1282,7 @@
				BE017C1D25AF568B00244ED8 /* retypepassdlg.cxx */,
				BE017C0125AF568A00244ED8 /* scuiautofmt.cxx */,
				BE017C1C25AF568B00244ED8 /* sharedocdlg.cxx */,
				BE017BFB25AF568900244ED8 /* shtabdlg.cxx */,
				BE017BFB25AF568900244ED8 /* gototabdlg.cxx */,
				BE017C1F25AF568B00244ED8 /* simpref.cxx */,
				BE017BF825AF568900244ED8 /* solveroptions.cxx */,
				BE017C0C25AF568A00244ED8 /* solverutil.cxx */,
@@ -2365,6 +2366,8 @@
		BEBF3E662465907000415E87 /* sw */ = {
			isa = PBXGroup;
			children = (
				29FD821228BC54AB00159078 /* txtnode */,
				29FD821128BC548D00159078 /* text */,
				BEBF3E6E246593A300415E87 /* ui */,
				BEBF3E672465907D00415E87 /* uibase */,
			);
diff --git a/sw/source/core/inc/drawfont.hxx b/sw/source/core/inc/drawfont.hxx
index c40ff10..9a02e88 100644
--- a/sw/source/core/inc/drawfont.hxx
+++ b/sw/source/core/inc/drawfont.hxx
@@ -61,6 +61,7 @@ class SW_DLLPUBLIC SwDrawTextInfo
    tools::Long m_nKanaDiff;
    TextFrameIndex m_nIdx;
    TextFrameIndex m_nLen;
    TextFrameIndex m_nMeasureLen;
    /// this is not a string index
    sal_Int32 m_nOfst;
    sal_uInt16 m_nWidth;
@@ -127,6 +128,7 @@ public:
        m_aText = rText;
        m_nIdx = nIdx;
        m_nLen = nLen;
        m_nMeasureLen = TextFrameIndex(COMPLETE_STRING);
        m_nKern = 0;
        m_nCompress = 0;
        m_nWidth = nWidth;
@@ -274,6 +276,11 @@ public:
        return m_nLen;
    }

    TextFrameIndex GetMeasureLen() const
    {
        return m_nMeasureLen;
    }

    sal_Int32 GetOffset() const
    {
#ifdef DBG_UTIL
@@ -488,6 +495,12 @@ public:
        m_nLen = nNew;
    }

    void SetMeasureLen(TextFrameIndex const nNew)
    {
        assert( nNew == TextFrameIndex(COMPLETE_STRING) || nNew <= m_nLen );
        m_nMeasureLen = nNew;
    }

    void SetOffset( sal_Int32 nNew )
    {
        m_nOfst = nNew;
diff --git a/sw/source/core/text/inftxt.cxx b/sw/source/core/text/inftxt.cxx
index 61a53c0..1ca1046 100644
--- a/sw/source/core/text/inftxt.cxx
+++ b/sw/source/core/text/inftxt.cxx
@@ -191,6 +191,7 @@ SwTextSizeInfo::SwTextSizeInfo()
, m_pText(nullptr)
, m_nIdx(0)
, m_nLen(0)
, m_nMeasureLen(COMPLETE_STRING)
, m_nKanaIdx(0)
, m_bOnWin    (false)
, m_bNotEOL   (false)
@@ -221,6 +222,7 @@ SwTextSizeInfo::SwTextSizeInfo( const SwTextSizeInfo &rNew )
      m_pText(&rNew.GetText()),
      m_nIdx(rNew.GetIdx()),
      m_nLen(rNew.GetLen()),
      m_nMeasureLen(rNew.GetMeasureLen()),
      m_nKanaIdx( rNew.GetKanaIdx() ),
      m_bOnWin( rNew.OnWin() ),
      m_bNotEOL( rNew.NotEOL() ),
@@ -309,7 +311,7 @@ void SwTextSizeInfo::CtorInitTextSizeInfo( OutputDevice* pRenderContext, SwTextF
    m_pText = &m_pFrame->GetText();

    m_nIdx = nNewIdx;
    m_nLen = TextFrameIndex(COMPLETE_STRING);
    m_nLen = m_nMeasureLen = TextFrameIndex(COMPLETE_STRING);
    m_bNotEOL = false;
    m_bStopUnderflow = m_bFootnoteInside = m_bOtherThanFootnoteInside = false;
    m_bMulti = m_bFirstMulti = m_bRuby = m_bHanging = m_bScriptSpace =
@@ -332,6 +334,7 @@ SwTextSizeInfo::SwTextSizeInfo( const SwTextSizeInfo &rNew, const OUString* pTex
      m_pText(pText),
      m_nIdx(nIndex),
      m_nLen(COMPLETE_STRING),
      m_nMeasureLen(COMPLETE_STRING),
      m_nKanaIdx( rNew.GetKanaIdx() ),
      m_bOnWin( rNew.OnWin() ),
      m_bNotEOL( rNew.NotEOL() ),
@@ -407,6 +410,7 @@ SwPosSize SwTextSizeInfo::GetTextSize() const
                                0 ;

    SwDrawTextInfo aDrawInf( m_pVsh, *m_pOut, &rSI, *m_pText, m_nIdx, m_nLen );
    aDrawInf.SetMeasureLen( m_nMeasureLen );
    aDrawInf.SetFrame( m_pFrame );
    aDrawInf.SetFont( m_pFnt );
    aDrawInf.SetSnapToGrid( SnapToGrid() );
@@ -1826,6 +1830,7 @@ SwTextSlot::SwTextSlot(
    , m_pOldGrammarCheckList(nullptr)
    , nIdx(0)
    , nLen(0)
    , nMeasureLen(0)
    , pInf(nullptr)
{
    if( rCh.isEmpty() )
@@ -1845,11 +1850,15 @@ SwTextSlot::SwTextSlot(
    pInf = const_cast<SwTextSizeInfo*>(pNew);
    nIdx = pInf->GetIdx();
    nLen = pInf->GetLen();
    nMeasureLen = pInf->GetMeasureLen();
    pOldText = &(pInf->GetText());
    m_pOldCachedVclData = pInf->GetCachedVclData();
    pInf->SetText( aText );
    pInf->SetIdx(TextFrameIndex(0));
    pInf->SetLen(bTextLen ? TextFrameIndex(pInf->GetText().getLength()) : pPor->GetLen());
    if (nMeasureLen != TextFrameIndex(COMPLETE_STRING))
        pInf->SetMeasureLen(TextFrameIndex(COMPLETE_STRING));

    pInf->SetCachedVclData(nullptr);

    // ST2
@@ -1923,6 +1932,7 @@ SwTextSlot::~SwTextSlot()
    pInf->SetText( *pOldText );
    pInf->SetIdx( nIdx );
    pInf->SetLen( nLen );
    pInf->SetMeasureLen( nMeasureLen );

    // ST2
    // Restore old smart tag list
diff --git a/sw/source/core/text/inftxt.hxx b/sw/source/core/text/inftxt.hxx
index 1621b4f..8011b29 100644
--- a/sw/source/core/text/inftxt.hxx
+++ b/sw/source/core/text/inftxt.hxx
@@ -154,6 +154,7 @@ protected:
    const OUString *m_pText;
    TextFrameIndex m_nIdx;
    TextFrameIndex m_nLen;
    TextFrameIndex m_nMeasureLen;
    sal_uInt16 m_nKanaIdx;
    bool m_bOnWin     : 1;
    bool m_bNotEOL    : 1;
@@ -273,6 +274,8 @@ public:
    void SetIdx(const TextFrameIndex nNew) { m_nIdx = nNew; }
    TextFrameIndex GetLen() const { return m_nLen; }
    void SetLen(const TextFrameIndex nNew) { m_nLen = nNew; }
    TextFrameIndex GetMeasureLen() const { return m_nMeasureLen; }
    void SetMeasureLen(const TextFrameIndex nNew) { m_nMeasureLen = nNew; }
    void SetText( const OUString &rNew ){ m_pText = &rNew; }

    // No Bullets for the symbol font!
@@ -682,6 +685,7 @@ class SwTextSlot final
    std::unique_ptr<sw::WrongListIterator> m_pTempIter;
    TextFrameIndex nIdx;
    TextFrameIndex nLen;
    TextFrameIndex nMeasureLen;
    bool bOn;
    SwTextSizeInfo *pInf;

diff --git a/sw/source/core/text/itrcrsr.cxx b/sw/source/core/text/itrcrsr.cxx
index f822065..47befa1 100644
--- a/sw/source/core/text/itrcrsr.cxx
+++ b/sw/source/core/text/itrcrsr.cxx
@@ -907,9 +907,21 @@ void SwTextCursor::GetCharRect_( SwRect* pOrig, TextFrameIndex const nOfst,
                    }
                    if ( pPor->PrtWidth() )
                    {
                        // tdf#30731: To get the correct nOfst width, we need
                        // to send the whole portion string to GetTextSize()
                        // and ask it to return the width of nOfst by calling
                        // SetMeasureLen(). Cutting the string at nOfst can
                        // give the wrong width if nOfst is in e.g. the middle
                        // of a ligature. See SwFntObj::DrawText().
                        TextFrameIndex const nOldLen = pPor->GetLen();
                        pPor->SetLen( nOfst - aInf.GetIdx() );
                        aInf.SetLen( pPor->GetLen() );
                        pPor->SetLen( nOfst - aInf.GetIdx() );
                        aInf.SetMeasureLen(pPor->GetLen());
                        if (aInf.GetLen() < aInf.GetMeasureLen())
                        {
                            pPor->SetLen(aInf.GetMeasureLen());
                            aInf.SetLen(pPor->GetLen());
                        }
                        if( nX || !pPor->InNumberGrp() )
                        {
                            SeekAndChg( aInf );
@@ -925,7 +937,12 @@ void SwTextCursor::GetCharRect_( SwRect* pOrig, TextFrameIndex const nOfst,
                            if( bWidth )
                            {
                                pPor->SetLen(pPor->GetLen() + TextFrameIndex(1));
                                aInf.SetLen( pPor->GetLen() );
                                aInf.SetMeasureLen(pPor->GetLen());
                                if (aInf.GetLen() < aInf.GetMeasureLen())
                                {
                                    pPor->SetLen(aInf.GetMeasureLen());
                                    aInf.SetLen(pPor->GetLen());
                                }
                                aInf.SetOnWin( false ); // no BULLETs!
                                nTmp += pPor->GetTextSize( aInf ).Width();
                                aInf.SetOnWin( bOldOnWin );
@@ -1102,8 +1119,14 @@ void SwTextCursor::GetCharRect_( SwRect* pOrig, TextFrameIndex const nOfst,
                    {
                        const bool bOldOnWin = aInf.OnWin();
                        TextFrameIndex const nOldLen = pPor->GetLen();
                        pPor->SetLen( TextFrameIndex(1) );
                        aInf.SetLen( pPor->GetLen() );
                        pPor->SetLen( TextFrameIndex(1) );
                        aInf.SetMeasureLen(pPor->GetLen());
                        if (aInf.GetLen() < aInf.GetMeasureLen())
                        {
                            pPor->SetLen(aInf.GetMeasureLen());
                            aInf.SetLen(pPor->GetLen());
                        }
                        SeekAndChg( aInf );
                        aInf.SetOnWin( false ); // no BULLETs!
                        aInf.SetKanaComp( pKanaComp );
diff --git a/sw/source/core/txtnode/fntcache.cxx b/sw/source/core/txtnode/fntcache.cxx
index 1461228..5e1a04f 100644
--- a/sw/source/core/txtnode/fntcache.cxx
+++ b/sw/source/core/txtnode/fntcache.cxx
@@ -732,23 +732,27 @@ static void lcl_DrawLineForWrongListData(
}

static void GetTextArray(const OutputDevice& rDevice, const OUString& rStr, std::vector<sal_Int32>& rDXAry,
                         sal_Int32 nIndex, sal_Int32 nLen, const vcl::text::TextLayoutCache* layoutCache = nullptr)
                         sal_Int32 nIndex, sal_Int32 nLen, bool bCaret = false,
                         const vcl::text::TextLayoutCache* layoutCache = nullptr)
{
    const SalLayoutGlyphs* pLayoutCache = SalLayoutGlyphsCache::self()->GetLayoutGlyphs(&rDevice, rStr, nIndex, nLen,
        0, layoutCache);
    rDevice.GetTextArray(rStr, &rDXAry, nIndex, nLen, layoutCache, pLayoutCache);
    rDevice.GetTextArray(rStr, &rDXAry, nIndex, nLen, bCaret, layoutCache, pLayoutCache);
}

static void GetTextArray(const OutputDevice& rOutputDevice, const SwDrawTextInfo& rInf, std::vector<sal_Int32>& rDXAry)
static void GetTextArray(const OutputDevice& rOutputDevice, const SwDrawTextInfo& rInf, std::vector<sal_Int32>& rDXAry,
                         bool bCaret = false)
{
    return GetTextArray(rOutputDevice, rInf.GetText(), rDXAry, rInf.GetIdx().get(), rInf.GetLen().get(), rInf.GetVclCache());
    return GetTextArray(rOutputDevice, rInf.GetText(), rDXAry, rInf.GetIdx().get(), rInf.GetLen().get(),
                        bCaret, rInf.GetVclCache());
}

static void GetTextArray(const OutputDevice& rOutputDevice, const SwDrawTextInfo& rInf, std::vector<sal_Int32>& rDXAry, sal_Int32 nLen)
static void GetTextArray(const OutputDevice& rOutputDevice, const SwDrawTextInfo& rInf, std::vector<sal_Int32>& rDXAry,
                         sal_Int32 nLen, bool bCaret = false)
{
    // Substring is fine.
    assert( nLen <= rInf.GetLen().get());
    return GetTextArray(rOutputDevice, rInf.GetText(), rDXAry, rInf.GetIdx().get(), nLen, rInf.GetVclCache());
    return GetTextArray(rOutputDevice, rInf.GetText(), rDXAry, rInf.GetIdx().get(), nLen, bCaret, rInf.GetVclCache());
}

void SwFntObj::DrawText( SwDrawTextInfo &rInf )
@@ -1529,6 +1533,15 @@ Size SwFntObj::GetTextSize( SwDrawTextInfo& rInf )
        ? rInf.GetLen()
        : TextFrameIndex(rInf.GetText().getLength());

    const TextFrameIndex nMsrLn = (TextFrameIndex(COMPLETE_STRING) != rInf.GetMeasureLen())
        ? rInf.GetMeasureLen()
        : nLn;

    // If the measure length is different from the length, then we are
    // measuring substring width for caret positioning, see SetMeasureLength()
    // use in TextCursor::GetCharRect_().
    bool bCaret(nMsrLn != nLn);

    // be sure to have the correct layout mode at the printer
    if ( m_pPrinter )
    {
@@ -1566,7 +1579,7 @@ Size SwFntObj::GetTextSize( SwDrawTextInfo& rInf )
                                GetFontLeading( rInf.GetShell(), rInf.GetOut() ) );

            std::vector<sal_Int32> aKernArray;
            GetTextArray(*pOutDev, rInf, aKernArray, sal_Int32(rInf.GetLen()));
            GetTextArray(*pOutDev, rInf, aKernArray, sal_Int32(nLn), bCaret);
            if (pGrid->IsSnapToChars())
            {
                sw::Justify::SnapToGrid(aKernArray, rInf.GetText(), sal_Int32(rInf.GetIdx()),
@@ -1579,7 +1592,7 @@ Size SwFntObj::GetTextSize( SwDrawTextInfo& rInf )
                        rInf.GetKern());
            }

            aTextSize.setWidth(aKernArray[sal_Int32(rInf.GetLen()) - 1]);
            aTextSize.setWidth(aKernArray[sal_Int32(nMsrLn) - 1]);
            rInf.SetKanaDiff( 0 );
            return aTextSize;
        }
@@ -1609,7 +1622,7 @@ Size SwFntObj::GetTextSize( SwDrawTextInfo& rInf )
            rInf.GetOut().SetFont( *m_pScrFont );

        GetTextArray(*m_pPrinter, rInf.GetText(), aKernArray,
                     sal_Int32(rInf.GetIdx()), sal_Int32(nLn));
                     sal_Int32(rInf.GetIdx()), sal_Int32(nLn), bCaret);
    }
    else
    {
@@ -1617,7 +1630,7 @@ Size SwFntObj::GetTextSize( SwDrawTextInfo& rInf )
            rInf.GetOut().SetFont( *m_pPrtFont );
        aTextSize.setHeight( rInf.GetOut().GetTextHeight() );

        GetTextArray(rInf.GetOut(), rInf, aKernArray, nLn.get());
        GetTextArray(rInf.GetOut(), rInf, aKernArray, nLn.get(), bCaret);
    }

    if (bCompress)
@@ -1628,16 +1641,16 @@ Size SwFntObj::GetTextSize( SwDrawTextInfo& rInf )
    else
        rInf.SetKanaDiff( 0 );

    if (nLn)
    if (nMsrLn)
    {
        aTextSize.setWidth(aKernArray[sal_Int32(nLn) - 1]);
        aTextSize.setWidth(aKernArray[sal_Int32(nMsrLn) - 1]);

        // Note that we can't simply use sal_Int(nLn) - 1 as nSpaceCount
        // Note that we can't simply use sal_Int(nMsrLn) - 1 as nSpaceCount
        // because a glyph may be made up of more than one characters.
        sal_Int32 nSpaceCount = 0;
        tools::Long nOldValue = aKernArray[0];

        for(sal_Int32 i = 1; i < sal_Int32(nLn); ++i)
        for(sal_Int32 i = 1; i < sal_Int32(nMsrLn); ++i)
        {
            if (nOldValue != aKernArray[i])
            {
diff --git a/sw/source/core/txtnode/swfont.cxx b/sw/source/core/txtnode/swfont.cxx
index 0ed7baf..c0149d9 100644
--- a/sw/source/core/txtnode/swfont.cxx
+++ b/sw/source/core/txtnode/swfont.cxx
@@ -986,8 +986,16 @@ Size SwSubFont::GetTextSize_( SwDrawTextInfo& rInf )
            ? TextFrameIndex(rInf.GetText().getLength())
            : rInf.GetLen();
    rInf.SetLen( nLn );

    if( IsCapital() && nLn )
    {
        if (rInf.GetMeasureLen() != TextFrameIndex(COMPLETE_STRING))
        {
            rInf.SetLen(rInf.GetMeasureLen());
            rInf.SetMeasureLen(TextFrameIndex(COMPLETE_STRING));
        }
        aTextSize = GetCapitalSize( rInf );
    }
    else
    {
        SV_STAT( nGetTextSize );
@@ -1009,17 +1017,25 @@ Size SwSubFont::GetTextSize_( SwDrawTextInfo& rInf )
                // a single snippet since its size may differ, too.
                TextFrameIndex const nOldIdx(rInf.GetIdx());
                TextFrameIndex const nOldLen(rInf.GetLen());
                TextFrameIndex const nOldMeasureLen(rInf.GetMeasureLen());
                const OUString aSnippet(oldStr.copy(sal_Int32(nOldIdx), sal_Int32(nOldLen)));
                const OUString aNewText(CalcCaseMap(aSnippet));

                rInf.SetText( aNewText );
                rInf.SetIdx( TextFrameIndex(0) );
                rInf.SetLen( TextFrameIndex(aNewText.getLength()) );
                if (nOldMeasureLen != TextFrameIndex(COMPLETE_STRING))
                {
                    const OUString aMeasureSnippet(oldStr.copy(sal_Int32(nOldIdx), sal_Int32(nOldMeasureLen)));
                    const OUString aNewMeasureText(CalcCaseMap(aMeasureSnippet));
                    rInf.SetMeasureLen(TextFrameIndex(aNewMeasureText.getLength()));
                }

                aTextSize = pLastFont->GetTextSize( rInf );

                rInf.SetIdx( nOldIdx );
                rInf.SetLen( nOldLen );
                rInf.SetMeasureLen(nOldMeasureLen);
            }
            else
            {
diff --git a/vcl/inc/sallayout.hxx b/vcl/inc/sallayout.hxx
index 165a617..0210161 100644
--- a/vcl/inc/sallayout.hxx
+++ b/vcl/inc/sallayout.hxx
@@ -62,7 +62,7 @@ class MultiSalLayout final : public SalLayout
public:
    void            DrawText(SalGraphics&) const override;
    sal_Int32       GetTextBreak(DeviceCoordinate nMaxWidth, DeviceCoordinate nCharExtra, int nFactor) const override;
    DeviceCoordinate FillDXArray(std::vector<DeviceCoordinate>* pDXArray) const override;
    DeviceCoordinate FillDXArray(std::vector<DeviceCoordinate>* pDXArray, const OUString& rStr) const override;
    void            GetCaretPositions(int nArraySize, sal_Int32* pCaretXArray) const override;
    bool            GetNextGlyph(const GlyphItem** pGlyph, DevicePoint& rPos, int& nStart,
                                 const LogicalFontInstance** ppGlyphFont = nullptr,
@@ -118,7 +118,7 @@ public:

    // used by upper layers
    DeviceCoordinate GetTextWidth() const final override;
    DeviceCoordinate FillDXArray(std::vector<DeviceCoordinate>* pDXArray) const final override;
    DeviceCoordinate FillDXArray(std::vector<DeviceCoordinate>* pDXArray, const OUString& rStr) const final override;
    sal_Int32 GetTextBreak(DeviceCoordinate nMaxWidth, DeviceCoordinate nCharExtra, int nFactor) const final override;
    void            GetCaretPositions(int nArraySize, sal_Int32* pCaretXArray) const final override;

@@ -145,7 +145,8 @@ private:
    void            Justify(DeviceCoordinate nNewWidth);
    void            ApplyAsianKerning(const OUString& rStr);

    void            GetCharWidths(std::vector<DeviceCoordinate>& rCharWidths) const;
    void            GetCharWidths(std::vector<DeviceCoordinate>& rCharWidths,
                                  const OUString& rStr) const;

    void            SetNeedFallback(vcl::text::ImplLayoutArgs&, sal_Int32, bool);

diff --git a/vcl/qa/cppunit/complextext.cxx b/vcl/qa/cppunit/complextext.cxx
index 6c35fd7..0b76f5e 100644
--- a/vcl/qa/cppunit/complextext.cxx
+++ b/vcl/qa/cppunit/complextext.cxx
@@ -52,12 +52,14 @@ public:
    void testTdf95650(); // Windows-only issue
    void testCaching();
    void testCachingSubstring();
    void testCaret();

    CPPUNIT_TEST_SUITE(VclComplexTextTest);
    CPPUNIT_TEST(testArabic);
    CPPUNIT_TEST(testTdf95650);
    CPPUNIT_TEST(testCaching);
    CPPUNIT_TEST(testCachingSubstring);
    CPPUNIT_TEST(testCaret);
    CPPUNIT_TEST_SUITE_END();
};

@@ -231,6 +233,86 @@ void VclComplexTextTest::testCachingSubstring()
    testCachedGlyphsSubstring( text, "Dejavu Sans", false );
}

void VclComplexTextTest::testCaret()
{
#if HAVE_MORE_FONTS
    ScopedVclPtrInstance<WorkWindow> pWin(static_cast<vcl::Window *>(nullptr));
    CPPUNIT_ASSERT( pWin );

    vcl::Font aFont("DejaVu Sans", "Book", Size(0, 200));

    OutputDevice *pOutDev = pWin->GetOutDev();
    pOutDev->SetFont( aFont );

    OUString aText;
    std::vector<sal_Int32> aCharWidths, aRefCharWidths;
    tools::Long nTextWidth, nTextWidth2;

    // A. RTL text
    aText = u"لا بلا";

    // 1) Regular DX array, the ligature width is given to the first components
    // and the next ones are all zero width.
    aRefCharWidths = { 114, 114, 178, 234, 353, 353 };
    aCharWidths.resize(aText.getLength());
    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
    nTextWidth = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, /*bCaret*/false);
    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
    CPPUNIT_ASSERT_EQUAL(tools::Long(353), nTextWidth);
    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());

    // 2) Caret placement DX array, ligature width is distributed over its
    // components.
    aRefCharWidths = { 57, 114, 178, 234, 293, 353 };
    aCharWidths.resize(aText.getLength());
    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
    nTextWidth = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, /*bCaret*/true);
    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
    CPPUNIT_ASSERT_EQUAL(tools::Long(353), nTextWidth);
    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());

    // 3) caret placement with combining marks, they should not add to ligature
    // component count.
    aText = u"لَاَ بلَاَ";
    aRefCharWidths = { 57, 57, 114, 114, 178, 234, 293, 293, 353, 353 };
    aCharWidths.resize(aText.getLength());
    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
    nTextWidth2 = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, /*bCaret*/true);
    CPPUNIT_ASSERT_EQUAL(aCharWidths[0], aCharWidths[1]);
    CPPUNIT_ASSERT_EQUAL(aCharWidths[2], aCharWidths[3]);
    CPPUNIT_ASSERT_EQUAL(aCharWidths[6], aCharWidths[7]);
    CPPUNIT_ASSERT_EQUAL(aCharWidths[8], aCharWidths[9]);
    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
    // FIXME: this should be 353, and the next assert should be true as well.
    CPPUNIT_ASSERT_EQUAL(tools::Long(388), nTextWidth2);
    //CPPUNIT_ASSERT_EQUAL(nTextWidth, nTextWidth2);
    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());

    // B. LTR text
    aText = u"fi fl ffi ffl";

    // 1) Regular DX array, the ligature width is given to the first components
    // and the next ones are all zero width.
    aRefCharWidths = { 126, 126, 190, 316, 316, 380, 573, 573, 573, 637, 830, 830, 830 };
    aCharWidths.resize(aText.getLength());
    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
    nTextWidth = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, /*bCaret*/false);
    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
    CPPUNIT_ASSERT_EQUAL(tools::Long(830), nTextWidth);
    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());

    // 2) Caret placement DX array, ligature width is distributed over its
    // components.
    aRefCharWidths = { 63, 126, 190, 253, 316, 380, 444, 508, 573, 637, 701, 765, 830 };
    aCharWidths.resize(aText.getLength());
    std::fill(aCharWidths.begin(), aCharWidths.end(), 0);
    nTextWidth = pOutDev->GetTextArray(aText, &aCharWidths, 0, -1, /*bCaret*/true);
    CPPUNIT_ASSERT_EQUAL(aRefCharWidths, aCharWidths);
    CPPUNIT_ASSERT_EQUAL(tools::Long(830), nTextWidth);
    CPPUNIT_ASSERT_EQUAL(sal_Int32(nTextWidth), aCharWidths.back());
#endif
}

CPPUNIT_TEST_SUITE_REGISTRATION(VclComplexTextTest);

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/source/gdi/CommonSalLayout.cxx b/vcl/source/gdi/CommonSalLayout.cxx
index fc28200..f75dc12 100644
--- a/vcl/source/gdi/CommonSalLayout.cxx
+++ b/vcl/source/gdi/CommonSalLayout.cxx
@@ -26,6 +26,7 @@
#include <vcl/unohelp.hxx>
#include <vcl/font/Feature.hxx>
#include <vcl/font/FeatureParser.hxx>
#include <vcl/svapp.hxx>

#include <ImplLayoutArgs.hxx>
#include <TextLayoutCache.hxx>
@@ -638,19 +639,58 @@ bool GenericSalLayout::LayoutText(vcl::text::ImplLayoutArgs& rArgs, const SalLay
    return true;
}

void GenericSalLayout::GetCharWidths(std::vector<DeviceCoordinate>& rCharWidths) const
void GenericSalLayout::GetCharWidths(std::vector<DeviceCoordinate>& rCharWidths, const OUString& rStr) const
{
    const int nCharCount = mnEndCharPos - mnMinCharPos;

    rCharWidths.clear();
    rCharWidths.resize(nCharCount, 0);

    css::uno::Reference<css::i18n::XBreakIterator> xBreak;
    auto aLocale(maLanguageTag.getLocale());

    for (auto const& aGlyphItem : m_GlyphItems)
    {
        const int nIndex = aGlyphItem.charPos() - mnMinCharPos;
        if (nIndex >= nCharCount)
        if (aGlyphItem.charPos() >= mnEndCharPos)
            continue;
        rCharWidths[nIndex] += aGlyphItem.newWidth();
        if (aGlyphItem.charCount() > 1 && aGlyphItem.newWidth() != 0 && !rStr.isEmpty())
        {
            // We are calculating DX array for cursor positions and this is a
            // ligature, we want to distribute the glyph width over the
            // ligature components.
            if (!xBreak.is())
                xBreak = mxBreak.is() ? mxBreak : vcl::unohelper::CreateBreakIterator();

            sal_Int32 nDone;
            sal_Int32 nPos = aGlyphItem.charPos();
            unsigned int nGraphemeCount = 0;

            // Count grapheme clusters in the ligatures.
            while (nPos < aGlyphItem.charPos() + aGlyphItem.charCount())
            {
                nPos = xBreak->nextCharacters(rStr, nPos, aLocale,
                    css::i18n::CharacterIteratorMode::SKIPCELL, 1, nDone);
                nGraphemeCount++;
            }

            // Set the width of each grapheme cluster.
            nPos = aGlyphItem.charPos();
            auto nWidth = aGlyphItem.newWidth() / nGraphemeCount;
            // rounding difference
            auto nDiff = aGlyphItem.newWidth() - (nWidth * nGraphemeCount);
            for (unsigned int i = 0; i < nGraphemeCount; i++)
            {
                rCharWidths[nPos - mnMinCharPos] += nWidth;
                // add rounding difference to last component to maintain
                // ligature width.
                if (i == nGraphemeCount - 1)
                    rCharWidths[nPos - mnMinCharPos] += nDiff;
                nPos = xBreak->nextCharacters(rStr, nPos, aLocale,
                    css::i18n::CharacterIteratorMode::SKIPCELL, 1, nDone);
            }
        }
        else
            rCharWidths[aGlyphItem.charPos() - mnMinCharPos] += aGlyphItem.newWidth();
    }
}

@@ -665,7 +705,7 @@ void GenericSalLayout::ApplyDXArray(const double* pDXArray, const sal_Bool* pKas
    std::unique_ptr<double[]> const pNewCharWidths(new double[nCharCount]);

    // Get the natural character widths (i.e. before applying DX adjustments).
    GetCharWidths(aOldCharWidths);
    GetCharWidths(aOldCharWidths, {});

    // Calculate the character widths after DX adjustments.
    for (int i = 0; i < nCharCount; ++i)
diff --git a/vcl/source/gdi/sallayout.cxx b/vcl/source/gdi/sallayout.cxx
index eca0714..6d22b30 100644
--- a/vcl/source/gdi/sallayout.cxx
+++ b/vcl/source/gdi/sallayout.cxx
@@ -133,6 +133,7 @@ sal_UCS4 GetLocalizedChar( sal_UCS4 nChar, LanguageType eLang )
SalLayout::SalLayout()
:   mnMinCharPos( -1 ),
    mnEndCharPos( -1 ),
    maLanguageTag( LANGUAGE_DONTKNOW ),
    mnUnitsPerPixel( 1 ),
    mnOrientation( 0 ),
    maDrawOffset( 0, 0 ),
@@ -147,6 +148,7 @@ void SalLayout::AdjustLayout( vcl::text::ImplLayoutArgs& rArgs )
    mnMinCharPos  = rArgs.mnMinCharPos;
    mnEndCharPos  = rArgs.mnEndCharPos;
    mnOrientation = rArgs.mnOrientation;
    maLanguageTag = rArgs.maLanguageTag;
}

DevicePoint SalLayout::GetDrawPosition(const DevicePoint& rRelative) const
@@ -263,10 +265,10 @@ SalLayoutGlyphs SalLayout::GetGlyphs() const
    return SalLayoutGlyphs(); // invalid
}

DeviceCoordinate GenericSalLayout::FillDXArray( std::vector<DeviceCoordinate>* pCharWidths ) const
DeviceCoordinate GenericSalLayout::FillDXArray( std::vector<DeviceCoordinate>* pCharWidths, const OUString& rStr ) const
{
    if (pCharWidths)
        GetCharWidths(*pCharWidths);
        GetCharWidths(*pCharWidths, rStr);

    return GetTextWidth();
}
@@ -494,7 +496,7 @@ void GenericSalLayout::GetCaretPositions( int nMaxIndex, sal_Int32* pCaretXArray
sal_Int32 GenericSalLayout::GetTextBreak( DeviceCoordinate nMaxWidth, DeviceCoordinate nCharExtra, int nFactor ) const
{
    std::vector<DeviceCoordinate> aCharWidths;
    GetCharWidths(aCharWidths);
    GetCharWidths(aCharWidths, {});

    DeviceCoordinate nWidth = 0;
    for( int i = mnMinCharPos; i < mnEndCharPos; ++i )
@@ -675,7 +677,7 @@ void MultiSalLayout::AdjustLayout( vcl::text::ImplLayoutArgs& rArgs )
            mpLayouts[n]->SalLayout::AdjustLayout( aMultiArgs );
        // then we can measure the unmodified metrics
        int nCharCount = rArgs.mnEndCharPos - rArgs.mnMinCharPos;
        FillDXArray( &aJustificationArray );
        FillDXArray( &aJustificationArray, {} );
        // #i17359# multilayout is not simplified yet, so calculating the
        // unjustified width needs handholding; also count the number of
        // stretchable virtual char widths
@@ -1039,12 +1041,12 @@ sal_Int32 MultiSalLayout::GetTextBreak( DeviceCoordinate nMaxWidth, DeviceCoordi
    int nCharCount = mnEndCharPos - mnMinCharPos;
    std::vector<DeviceCoordinate> aCharWidths;
    std::vector<DeviceCoordinate> aFallbackCharWidths;
    mpLayouts[0]->FillDXArray( &aCharWidths );
    mpLayouts[0]->FillDXArray( &aCharWidths, {} );

    for( int n = 1; n < mnLevel; ++n )
    {
        SalLayout& rLayout = *mpLayouts[ n ];
        rLayout.FillDXArray( &aFallbackCharWidths );
        rLayout.FillDXArray( &aFallbackCharWidths, {} );
        double fUnitMul = mnUnitsPerPixel;
        fUnitMul /= rLayout.GetUnitsPerPixel();
        for( int i = 0; i < nCharCount; ++i )
@@ -1070,7 +1072,7 @@ sal_Int32 MultiSalLayout::GetTextBreak( DeviceCoordinate nMaxWidth, DeviceCoordi
    return -1;
}

DeviceCoordinate MultiSalLayout::FillDXArray( std::vector<DeviceCoordinate>* pCharWidths ) const
DeviceCoordinate MultiSalLayout::FillDXArray( std::vector<DeviceCoordinate>* pCharWidths, const OUString& rStr ) const
{
    DeviceCoordinate nMaxWidth = 0;

@@ -1086,7 +1088,7 @@ DeviceCoordinate MultiSalLayout::FillDXArray( std::vector<DeviceCoordinate>* pCh
    for( int n = mnLevel; --n >= 0; )
    {
        // query every fallback level
        DeviceCoordinate nTextWidth = mpLayouts[n]->FillDXArray( &aTempWidths );
        DeviceCoordinate nTextWidth = mpLayouts[n]->FillDXArray( &aTempWidths, rStr );
        if( !nTextWidth )
            continue;
        // merge results from current level
diff --git a/vcl/source/outdev/text.cxx b/vcl/source/outdev/text.cxx
index 0cf0283..d5969c7 100644
--- a/vcl/source/outdev/text.cxx
+++ b/vcl/source/outdev/text.cxx
@@ -889,7 +889,7 @@ tools::Long OutputDevice::GetTextWidth( const OUString& rStr, sal_Int32 nIndex, 
{

    tools::Long nWidth = GetTextArray( rStr, nullptr, nIndex,
            nLen, pLayoutCache, pSalLayoutCache );
            nLen, false, pLayoutCache, pSalLayoutCache );

    return nWidth;
}
@@ -956,7 +956,7 @@ void OutputDevice::DrawTextArray( const Point& rStartPt, const OUString& rStr,
}

tools::Long OutputDevice::GetTextArray( const OUString& rStr, std::vector<sal_Int32>* pDXAry,
                                 sal_Int32 nIndex, sal_Int32 nLen,
                                 sal_Int32 nIndex, sal_Int32 nLen, bool bCaret,
                                 vcl::text::TextLayoutCache const*const pLayoutCache,
                                 SalLayoutGlyphs const*const pSalLayoutCache) const
{
@@ -994,7 +994,7 @@ tools::Long OutputDevice::GetTextArray( const OUString& rStr, std::vector<sal_In
        xDXPixelArray.reset(new std::vector<DeviceCoordinate>(nLen));
    }
    std::vector<DeviceCoordinate>* pDXPixelArray = xDXPixelArray.get();
    DeviceCoordinate nWidth = pSalLayout->FillDXArray(pDXPixelArray);
    DeviceCoordinate nWidth = pSalLayout->FillDXArray(pDXPixelArray, bCaret ? rStr : OUString());
    int nWidthFactor = pSalLayout->GetUnitsPerPixel();

    // convert virtual char widths to virtual absolute positions
@@ -1039,7 +1039,7 @@ tools::Long OutputDevice::GetTextArray( const OUString& rStr, std::vector<sal_In

#else /* ! VCL_FLOAT_DEVICE_PIXEL */

    tools::Long nWidth = pSalLayout->FillDXArray( pDXAry );
    tools::Long nWidth = pSalLayout->FillDXArray( pDXAry, bCaret ? rStr : OUString() );
    int nWidthFactor = pSalLayout->GetUnitsPerPixel();

    // convert virtual char widths to virtual absolute positions