diff options
Diffstat (limited to 'vcl/source/text')
-rw-r--r-- | vcl/source/text/ImplLayoutArgs.cxx | 363 | ||||
-rw-r--r-- | vcl/source/text/ImplLayoutRuns.cxx | 172 | ||||
-rw-r--r-- | vcl/source/text/TextLayoutCache.cxx | 75 | ||||
-rw-r--r-- | vcl/source/text/mnemonic.cxx | 53 | ||||
-rw-r--r-- | vcl/source/text/textlayout.cxx | 746 |
5 files changed, 1409 insertions, 0 deletions
diff --git a/vcl/source/text/ImplLayoutArgs.cxx b/vcl/source/text/ImplLayoutArgs.cxx new file mode 100644 index 0000000000..826d288bba --- /dev/null +++ b/vcl/source/text/ImplLayoutArgs.cxx @@ -0,0 +1,363 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <ImplLayoutArgs.hxx> + +#include <unicode/ubidi.h> +#include <unicode/uchar.h> + +#include <algorithm> +#include <memory> +#include <utility> + +namespace vcl::text +{ +ImplLayoutArgs::ImplLayoutArgs(const OUString& rStr, int nMinCharPos, int nEndCharPos, + SalLayoutFlags nFlags, LanguageTag aLanguageTag, + vcl::text::TextLayoutCache const* const pLayoutCache) + : maLanguageTag(std::move(aLanguageTag)) + , mnFlags(nFlags) + , mrStr(rStr) + , mnMinCharPos(nMinCharPos) + , mnEndCharPos(nEndCharPos) + , m_pTextLayoutCache(pLayoutCache) + , mpDXArray(nullptr) + , mpKashidaArray(nullptr) + , mnLayoutWidth(0) + , mnOrientation(0) +{ + if (mnFlags & SalLayoutFlags::BiDiStrong) + { + // handle strong BiDi mode + + // do not bother to BiDi analyze strong LTR/RTL + // TODO: can we assume these strings do not have unicode control chars? + // if not remove the control characters from the runs + bool bRTL(mnFlags & SalLayoutFlags::BiDiRtl); + AddRun(mnMinCharPos, mnEndCharPos, bRTL); + } + else + { + // handle weak BiDi mode + UBiDiLevel nLevel = (mnFlags & SalLayoutFlags::BiDiRtl) ? 1 : 0; + + // prepare substring for BiDi analysis + // TODO: reuse allocated pParaBidi + UErrorCode rcI18n = U_ZERO_ERROR; + const int nLength = mnEndCharPos - mnMinCharPos; + UBiDi* pParaBidi = ubidi_openSized(nLength, 0, &rcI18n); + if (!pParaBidi) + return; + ubidi_setPara(pParaBidi, reinterpret_cast<const UChar*>(mrStr.getStr()) + mnMinCharPos, + nLength, nLevel, nullptr, &rcI18n); + + // run BiDi algorithm + const int nRunCount = ubidi_countRuns(pParaBidi, &rcI18n); + for (int i = 0; i < nRunCount; ++i) + { + int32_t nMinPos, nRunLength; + const UBiDiDirection nDir = ubidi_getVisualRun(pParaBidi, i, &nMinPos, &nRunLength); + const int nPos0 = nMinPos + mnMinCharPos; + const int nPos1 = nPos0 + nRunLength; + + const bool bRTL = (nDir == UBIDI_RTL); + AddRun(nPos0, nPos1, bRTL); + } + + // cleanup BiDi engine + ubidi_close(pParaBidi); + } + + // prepare calls to GetNextPos/GetNextRun + maRuns.ResetPos(); +} + +void ImplLayoutArgs::SetLayoutWidth(double nWidth) { mnLayoutWidth = nWidth; } + +void ImplLayoutArgs::SetDXArray(double const* pDXArray) { mpDXArray = pDXArray; } + +void ImplLayoutArgs::SetKashidaArray(sal_Bool const* pKashidaArray) +{ + mpKashidaArray = pKashidaArray; +} + +void ImplLayoutArgs::SetOrientation(Degree10 nOrientation) { mnOrientation = nOrientation; } + +void ImplLayoutArgs::ResetPos() { maRuns.ResetPos(); } + +bool ImplLayoutArgs::GetNextPos(int* nCharPos, bool* bRTL) +{ + return maRuns.GetNextPos(nCharPos, bRTL); +} + +void ImplLayoutArgs::AddFallbackRun(int nMinRunPos, int nEndRunPos, bool bRTL) +{ + maFallbackRuns.AddRun(nMinRunPos, nEndRunPos, bRTL); +} + +bool ImplLayoutArgs::HasFallbackRun() const { return !maFallbackRuns.IsEmpty(); } + +static bool IsControlChar(sal_UCS4 cChar) +{ + // C0 control characters + if ((0x0001 <= cChar) && (cChar <= 0x001F)) + return true; + // formatting characters + if ((0x200E <= cChar) && (cChar <= 0x200F)) + return true; + if ((0x2028 <= cChar) && (cChar <= 0x202E)) + return true; + // deprecated formatting characters + if ((0x206A <= cChar) && (cChar <= 0x206F)) + return true; + if (0x2060 == cChar) + return true; + // byte order markers and invalid unicode + if ((cChar == 0xFEFF) || (cChar == 0xFFFE) || (cChar == 0xFFFF)) + return true; + // drop null character too, broken documents may contain it (ofz34898-1.doc) + if (cChar == 0) + return true; + return false; +} + +// add a run after splitting it up to get rid of control chars +void ImplLayoutArgs::AddRun(int nCharPos0, int nCharPos1, bool bRTL) +{ + SAL_WARN_IF(nCharPos0 > nCharPos1, "vcl", "ImplLayoutArgs::AddRun() nCharPos0>=nCharPos1"); + + // remove control characters from runs by splitting them up + if (!bRTL) + { + for (int i = nCharPos0; i < nCharPos1; ++i) + if (IsControlChar(mrStr[i])) + { + // add run until control char + maRuns.AddRun(nCharPos0, i, bRTL); + nCharPos0 = i + 1; + } + } + else + { + for (int i = nCharPos1; --i >= nCharPos0;) + if (IsControlChar(mrStr[i])) + { + // add run until control char + maRuns.AddRun(i + 1, nCharPos1, bRTL); + nCharPos1 = i; + } + } + + // add remainder of run + maRuns.AddRun(nCharPos0, nCharPos1, bRTL); +} + +bool ImplLayoutArgs::PrepareFallback(const SalLayoutGlyphsImpl* pGlyphsImpl) +{ + // Generate runs with pre-calculated glyph items instead maFallbackRuns. + if (pGlyphsImpl != nullptr) + { + maRuns.Clear(); + maFallbackRuns.Clear(); + + for (auto const& aGlyphItem : *pGlyphsImpl) + { + for (int i = aGlyphItem.charPos(); i < aGlyphItem.charPos() + aGlyphItem.charCount(); + ++i) + maRuns.AddPos(i, aGlyphItem.IsRTLGlyph()); + } + + return !maRuns.IsEmpty(); + } + + // short circuit if no fallback is needed + if (maFallbackRuns.IsEmpty()) + { + maRuns.Clear(); + return false; + } + + // convert the fallback requests to layout requests + bool bRTL; + int nMin, nEnd; + + // get the individual fallback requests + std::vector<int> aPosVector; + aPosVector.reserve(mrStr.getLength()); + maFallbackRuns.ResetPos(); + for (; maFallbackRuns.GetRun(&nMin, &nEnd, &bRTL); maFallbackRuns.NextRun()) + for (int i = nMin; i < nEnd; ++i) + aPosVector.push_back(i); + maFallbackRuns.Clear(); + + // sort the individual fallback requests + std::sort(aPosVector.begin(), aPosVector.end()); + + // adjust fallback runs to have the same order and limits of the original runs + ImplLayoutRuns aNewRuns; + maRuns.ResetPos(); + for (; maRuns.GetRun(&nMin, &nEnd, &bRTL); maRuns.NextRun()) + { + if (!bRTL) + { + auto it = std::lower_bound(aPosVector.begin(), aPosVector.end(), nMin); + for (; (it != aPosVector.end()) && (*it < nEnd); ++it) + aNewRuns.AddPos(*it, bRTL); + } + else + { + auto it = std::upper_bound(aPosVector.begin(), aPosVector.end(), nEnd); + while ((it != aPosVector.begin()) && (*--it >= nMin)) + aNewRuns.AddPos(*it, bRTL); + } + } + + maRuns = aNewRuns; // TODO: use vector<>::swap() + maRuns.ResetPos(); + return true; +} + +bool ImplLayoutArgs::GetNextRun(int* nMinRunPos, int* nEndRunPos, bool* bRTL) +{ + bool bValid = maRuns.GetRun(nMinRunPos, nEndRunPos, bRTL); + maRuns.NextRun(); + return bValid; +} +} + +std::ostream& operator<<(std::ostream& s, vcl::text::ImplLayoutArgs const& rArgs) +{ +#ifndef SAL_LOG_INFO + (void)rArgs; +#else + s << "ImplLayoutArgs{"; + + s << "Flags="; + if (rArgs.mnFlags == SalLayoutFlags::NONE) + s << 0; + else + { + bool need_or = false; + s << "{"; +#define TEST(x) \ + if (rArgs.mnFlags & SalLayoutFlags::x) \ + { \ + if (need_or) \ + s << "|"; \ + s << #x; \ + need_or = true; \ + } + TEST(BiDiRtl); + TEST(BiDiStrong); + TEST(RightAlign); + TEST(DisableKerning); + TEST(KerningAsian); + TEST(Vertical); + TEST(DisableLigatures); + TEST(ForFallback); +#undef TEST + s << "}"; + } + + const int nLength = rArgs.mrStr.getLength(); + + s << ",Length=" << nLength; + s << ",MinCharPos=" << rArgs.mnMinCharPos; + s << ",EndCharPos=" << rArgs.mnEndCharPos; + + s << ",Str=\""; + int lim = nLength; + if (lim > 10) + lim = 7; + for (int i = 0; i < lim; i++) + { + if (rArgs.mrStr[i] == '\n') + s << "\\n"; + else if (rArgs.mrStr[i] < ' ' || (rArgs.mrStr[i] >= 0x7F && rArgs.mrStr[i] <= 0xFF)) + s << "\\0x" << std::hex << std::setw(2) << std::setfill('0') + << static_cast<int>(rArgs.mrStr[i]) << std::setfill(' ') << std::setw(1) << std::dec; + else if (rArgs.mrStr[i] < 0x7F) + s << static_cast<char>(rArgs.mrStr[i]); + else + s << "\\u" << std::hex << std::setw(4) << std::setfill('0') + << static_cast<int>(rArgs.mrStr[i]) << std::setfill(' ') << std::setw(1) << std::dec; + } + if (nLength > lim) + s << "..."; + s << "\""; + + s << ",DXArray="; + if (rArgs.mpDXArray) + { + s << "["; + int count = rArgs.mnEndCharPos - rArgs.mnMinCharPos; + lim = count; + if (lim > 10) + lim = 7; + for (int i = 0; i < lim; i++) + { + s << rArgs.mpDXArray[i]; + if (i < lim - 1) + s << ","; + } + if (count > lim) + { + if (count > lim + 1) + s << "..."; + s << rArgs.mpDXArray[count - 1]; + } + s << "]"; + } + else + s << "NULL"; + + s << ",KashidaArray="; + if (rArgs.mpKashidaArray) + { + s << "["; + int count = rArgs.mnEndCharPos - rArgs.mnMinCharPos; + lim = count; + if (lim > 10) + lim = 7; + for (int i = 0; i < lim; i++) + { + s << rArgs.mpKashidaArray[i]; + if (i < lim - 1) + s << ","; + } + if (count > lim) + { + if (count > lim + 1) + s << "..."; + s << rArgs.mpKashidaArray[count - 1]; + } + s << "]"; + } + else + s << "NULL"; + + s << ",LayoutWidth=" << rArgs.mnLayoutWidth; + + s << "}"; + +#endif + return s; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/text/ImplLayoutRuns.cxx b/vcl/source/text/ImplLayoutRuns.cxx new file mode 100644 index 0000000000..3e054f5c40 --- /dev/null +++ b/vcl/source/text/ImplLayoutRuns.cxx @@ -0,0 +1,172 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <ImplLayoutRuns.hxx> +#include <algorithm> + +void ImplLayoutRuns::AddPos( int nCharPos, bool bRTL ) +{ + // check if charpos could extend current run + int nIndex = maRuns.size(); + if( nIndex >= 2 ) + { + int nRunPos0 = maRuns[ nIndex-2 ]; + int nRunPos1 = maRuns[ nIndex-1 ]; + if( ((nCharPos + int(bRTL)) == nRunPos1) && ((nRunPos0 > nRunPos1) == bRTL) ) + { + // extend current run by new charpos + maRuns[ nIndex-1 ] = nCharPos + int(!bRTL); + return; + } + // ignore new charpos when it is in current run + if( (nRunPos0 <= nCharPos) && (nCharPos < nRunPos1) ) + return; + if( (nRunPos1 <= nCharPos) && (nCharPos < nRunPos0) ) + return; + } + + // else append a new run consisting of the new charpos + maRuns.push_back( nCharPos + (bRTL ? 1 : 0) ); + maRuns.push_back( nCharPos + (bRTL ? 0 : 1) ); +} + +void ImplLayoutRuns::AddRun( int nCharPos0, int nCharPos1, bool bRTL ) +{ + if( nCharPos0 == nCharPos1 ) + return; + + // swap if needed + if( bRTL == (nCharPos0 < nCharPos1) ) + std::swap( nCharPos0, nCharPos1 ); + + if (maRuns.size() >= 2 && nCharPos0 == maRuns[maRuns.size() - 2] && nCharPos1 == maRuns[maRuns.size() - 1]) + { + //this run is the same as the last + return; + } + + // append new run + maRuns.push_back( nCharPos0 ); + maRuns.push_back( nCharPos1 ); +} + +bool ImplLayoutRuns::PosIsInRun( int nCharPos ) const +{ + if( mnRunIndex >= static_cast<int>(maRuns.size()) ) + return false; + + int nMinCharPos = maRuns[ mnRunIndex+0 ]; + int nEndCharPos = maRuns[ mnRunIndex+1 ]; + if( nMinCharPos > nEndCharPos ) // reversed in RTL case + std::swap( nMinCharPos, nEndCharPos ); + + if( nCharPos < nMinCharPos ) + return false; + if( nCharPos >= nEndCharPos ) + return false; + return true; +} + +bool ImplLayoutRuns::PosIsInAnyRun( int nCharPos ) const +{ + bool bRet = false; + int nRunIndex = mnRunIndex; + + ImplLayoutRuns *pThis = const_cast<ImplLayoutRuns*>(this); + + pThis->ResetPos(); + + for (size_t i = 0; i < maRuns.size(); i+=2) + { + bRet = PosIsInRun( nCharPos ); + if( bRet ) + break; + pThis->NextRun(); + } + + pThis->mnRunIndex = nRunIndex; + return bRet; +} + +bool ImplLayoutRuns::GetNextPos( int* nCharPos, bool* bRightToLeft ) +{ + // negative nCharPos => reset to first run + if( *nCharPos < 0 ) + mnRunIndex = 0; + + // return false when all runs completed + if( mnRunIndex >= static_cast<int>(maRuns.size()) ) + return false; + + int nRunPos0 = maRuns[ mnRunIndex+0 ]; + int nRunPos1 = maRuns[ mnRunIndex+1 ]; + *bRightToLeft = (nRunPos0 > nRunPos1); + + if( *nCharPos < 0 ) + { + // get first valid nCharPos in run + *nCharPos = nRunPos0; + } + else + { + // advance to next nCharPos for LTR case + if( !*bRightToLeft ) + ++(*nCharPos); + + // advance to next run if current run is completed + if( *nCharPos == nRunPos1 ) + { + if( (mnRunIndex += 2) >= static_cast<int>(maRuns.size()) ) + return false; + nRunPos0 = maRuns[ mnRunIndex+0 ]; + nRunPos1 = maRuns[ mnRunIndex+1 ]; + *bRightToLeft = (nRunPos0 > nRunPos1); + *nCharPos = nRunPos0; + } + } + + // advance to next nCharPos for RTL case + if( *bRightToLeft ) + --(*nCharPos); + + return true; +} + +bool ImplLayoutRuns::GetRun( int* nMinRunPos, int* nEndRunPos, bool* bRightToLeft ) const +{ + if( mnRunIndex >= static_cast<int>(maRuns.size()) ) + return false; + + int nRunPos0 = maRuns[ mnRunIndex+0 ]; + int nRunPos1 = maRuns[ mnRunIndex+1 ]; + *bRightToLeft = (nRunPos1 < nRunPos0) ; + if( !*bRightToLeft ) + { + *nMinRunPos = nRunPos0; + *nEndRunPos = nRunPos1; + } + else + { + *nMinRunPos = nRunPos1; + *nEndRunPos = nRunPos0; + } + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/vcl/source/text/TextLayoutCache.cxx b/vcl/source/text/TextLayoutCache.cxx new file mode 100644 index 0000000000..1d3e8e5045 --- /dev/null +++ b/vcl/source/text/TextLayoutCache.cxx @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <TextLayoutCache.hxx> + +#include <scrptrun.h> + +#include <o3tl/hash_combine.hxx> +#include <o3tl/lru_map.hxx> +#include <unotools/configmgr.hxx> +#include <vcl/lazydelete.hxx> +#include <officecfg/Office/Common.hxx> + +namespace vcl::text +{ +TextLayoutCache::TextLayoutCache(sal_Unicode const* pStr, sal_Int32 const nEnd) +{ + vcl::ScriptRun aScriptRun(reinterpret_cast<const UChar*>(pStr), nEnd); + while (aScriptRun.next()) + { + runs.emplace_back(aScriptRun.getScriptStart(), aScriptRun.getScriptEnd(), + aScriptRun.getScriptCode()); + } +} + +namespace +{ +struct TextLayoutCacheCost +{ + size_t operator()(const std::shared_ptr<const TextLayoutCache>& item) const + { + return item->runs.size() * sizeof(item->runs.front()); + } +}; +} // namespace + +std::shared_ptr<const TextLayoutCache> TextLayoutCache::Create(OUString const& rString) +{ + typedef o3tl::lru_map<OUString, std::shared_ptr<const TextLayoutCache>, FirstCharsStringHash, + FastStringCompareEqual, TextLayoutCacheCost> + Cache; + static vcl::DeleteOnDeinit<Cache> cache( + !utl::ConfigManager::IsFuzzing() + ? officecfg::Office::Common::Cache::Font::TextRunsCacheSize::get() + : 100); + if (Cache* map = cache.get()) + { + auto it = map->find(rString); + if (it != map->end()) + return it->second; + auto ret = std::make_shared<const TextLayoutCache>(rString.getStr(), rString.getLength()); + map->insert({ rString, ret }); + return ret; + } + return std::make_shared<const TextLayoutCache>(rString.getStr(), rString.getLength()); +} +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/source/text/mnemonic.cxx b/vcl/source/text/mnemonic.cxx new file mode 100644 index 0000000000..7336032b8d --- /dev/null +++ b/vcl/source/text/mnemonic.cxx @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <vcl/mnemonic.hxx> + +OUString removeMnemonicFromString(OUString const& rStr) +{ + sal_Int32 nDummy; + return removeMnemonicFromString(rStr, nDummy); +} + +OUString removeMnemonicFromString(OUString const& rStr, sal_Int32& rMnemonicPos) +{ + OUString aStr = rStr; + sal_Int32 nLen = aStr.getLength(); + sal_Int32 i = 0; + + rMnemonicPos = -1; + while (i < nLen) + { + if (aStr[i] == '~') + { + if (nLen <= i + 1) + break; + + if (aStr[i + 1] != '~') + { + if (rMnemonicPos == -1) + rMnemonicPos = i; + aStr = aStr.replaceAt(i, 1, u""); + nLen--; + } + else + { + aStr = aStr.replaceAt(i, 1, u""); + nLen--; + i++; + } + } + else + i++; + } + + return aStr; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ diff --git a/vcl/source/text/textlayout.cxx b/vcl/source/text/textlayout.cxx new file mode 100644 index 0000000000..33232b6f09 --- /dev/null +++ b/vcl/source/text/textlayout.cxx @@ -0,0 +1,746 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <osl/file.h> +#include <rtl/ustrbuf.hxx> +#include <sal/log.hxx> +#include <comphelper/processfactory.hxx> +#include <i18nlangtag/languagetag.hxx> + +#include <vcl/ctrl.hxx> +#include <vcl/svapp.hxx> +#include <vcl/unohelp.hxx> + +#include <textlayout.hxx> +#include <textlineinfo.hxx> + +static bool lcl_IsCharIn(sal_Unicode c, const char* pStr) +{ + while ( *pStr ) + { + if ( *pStr == c ) + return true; + pStr++; + } + + return false; +} + +ImplMultiTextLineInfo::ImplMultiTextLineInfo() +{ +} + +ImplMultiTextLineInfo::~ImplMultiTextLineInfo() +{ +} + +void ImplMultiTextLineInfo::AddLine( const ImplTextLineInfo& rLine ) +{ + mvLines.push_back(rLine); +} + +void ImplMultiTextLineInfo::Clear() +{ + mvLines.clear(); +} + +namespace vcl +{ + OUString TextLayoutCommon::GetCenterEllipsisString(OUString const& rOrigStr, sal_Int32 nIndex, tools::Long nMaxWidth) + { + OUStringBuffer aTmpStr(rOrigStr); + + // speed it up by removing all but 1.33x as many as the break pos. + sal_Int32 nEraseChars = std::max<sal_Int32>(4, rOrigStr.getLength() - (nIndex*4)/3); + while(nEraseChars < rOrigStr.getLength() && GetTextWidth(aTmpStr.toString(), 0, aTmpStr.getLength()) > nMaxWidth) + { + aTmpStr = rOrigStr; + sal_Int32 i = (aTmpStr.getLength() - nEraseChars)/2; + aTmpStr.remove(i, nEraseChars++); + aTmpStr.insert(i, "..."); + } + + return aTmpStr.makeStringAndClear(); + } + + OUString TextLayoutCommon::GetEndEllipsisString(OUString const& rOrigStr, sal_Int32 nIndex, tools::Long nMaxWidth, bool bClipText) + { + OUString aStr = rOrigStr; + aStr = aStr.copy(0, nIndex); + + if (nIndex > 1) + { + aStr += "..."; + while (!aStr.isEmpty() && (GetTextWidth(aStr, 0, aStr.getLength()) > nMaxWidth)) + { + if ((nIndex > 1) || (nIndex == aStr.getLength())) + nIndex--; + + aStr = aStr.replaceAt(nIndex, 1, u""); + } + } + + if (aStr.isEmpty() && bClipText) + aStr += OUStringChar(rOrigStr[0]); + + return aStr; + } + + namespace + { + OUString lcl_GetPathEllipsisString(OUString const& rOrigStr, sal_Int32 nIndex) + { + OUString aPath(rOrigStr); + OUString aAbbreviatedPath; + osl_abbreviateSystemPath(aPath.pData, &aAbbreviatedPath.pData, nIndex, nullptr); + return aAbbreviatedPath; + } + } + + OUString TextLayoutCommon::GetNewsEllipsisString(OUString const& rOrigStr, tools::Long nMaxWidth, DrawTextFlags nStyle) + { + OUString aStr = rOrigStr; + static char const pSepChars[] = "."; + + // Determine last section + sal_Int32 nLastContent = aStr.getLength(); + while (nLastContent) + { + nLastContent--; + + if (lcl_IsCharIn(aStr[nLastContent], pSepChars)) + break; + } + + while (nLastContent && lcl_IsCharIn(aStr[nLastContent-1], pSepChars)) + { + nLastContent--; + } + + OUString aLastStr = aStr.copy(nLastContent); + OUString aTempLastStr1 = "..." + aLastStr; + + if (GetTextWidth(aTempLastStr1, 0, aTempLastStr1.getLength()) > nMaxWidth) + return GetEllipsisString(aStr, nMaxWidth, DrawTextFlags::EndEllipsis); + + sal_Int32 nFirstContent = 0; + while (nFirstContent < nLastContent) + { + nFirstContent++; + if (lcl_IsCharIn(aStr[nFirstContent], pSepChars)) + break; + } + + while ((nFirstContent < nLastContent) && lcl_IsCharIn(aStr[nFirstContent], pSepChars)) + { + nFirstContent++; + } + + if (nFirstContent >= nLastContent) + return GetEllipsisString(aStr, nMaxWidth, nStyle | DrawTextFlags::EndEllipsis); + + if (nFirstContent > 4) + nFirstContent = 4; + + OUString aFirstStr = OUString::Concat(aStr.subView(0, nFirstContent)) + "..."; + OUString aTempStr = aFirstStr + aLastStr; + + if (GetTextWidth(aTempStr, 0, aTempStr.getLength() ) > nMaxWidth) + return GetEllipsisString(aStr, nMaxWidth, nStyle | DrawTextFlags::EndEllipsis); + + do + { + aStr = aTempStr; + + if (nLastContent > aStr.getLength()) + nLastContent = aStr.getLength(); + + while (nFirstContent < nLastContent) + { + nLastContent--; + if (lcl_IsCharIn(aStr[nLastContent], pSepChars)) + break; + + } + + while ((nFirstContent < nLastContent) && lcl_IsCharIn(aStr[nLastContent-1], pSepChars)) + { + nLastContent--; + } + + if (nFirstContent < nLastContent) + { + std::u16string_view aTempLastStr = aStr.subView(nLastContent); + aTempStr = aFirstStr + aTempLastStr; + + if (GetTextWidth(aTempStr, 0, aTempStr.getLength()) > nMaxWidth) + break; + } + } + while (nFirstContent < nLastContent); + + return aStr; + } + + OUString TextLayoutCommon::GetEllipsisString(OUString const& rOrigStr, tools::Long nMaxWidth, DrawTextFlags nStyle) + { + OUString aStr = rOrigStr; + sal_Int32 nIndex = GetTextBreak( aStr, nMaxWidth, 0, aStr.getLength() ); + + if (nIndex == -1) + return aStr; + + if ((nStyle & DrawTextFlags::CenterEllipsis) == DrawTextFlags::CenterEllipsis) + aStr = GetCenterEllipsisString(rOrigStr, nIndex, nMaxWidth); + else if (nStyle & DrawTextFlags::EndEllipsis) + aStr = GetEndEllipsisString(rOrigStr, nIndex, nMaxWidth, (nStyle & DrawTextFlags::Clip) == DrawTextFlags::Clip); + else if (nStyle & DrawTextFlags::PathEllipsis) + aStr = lcl_GetPathEllipsisString(rOrigStr, nIndex); + else if ( nStyle & DrawTextFlags::NewsEllipsis ) + aStr = GetNewsEllipsisString(rOrigStr, nMaxWidth, nStyle); + + return aStr; + } + + sal_Int32 TextLayoutCommon::BreakLinesWithIterator(const tools::Long nWidth, OUString const& rStr, + css::uno::Reference< css::linguistic2::XHyphenator > const& xHyph, + css::uno::Reference<css::i18n::XBreakIterator> const& xBI, + const bool bHyphenate, + const sal_Int32 nPos, sal_Int32 nBreakPos) + { + const css::lang::Locale& rDefLocale(Application::GetSettings().GetUILanguageTag().getLocale()); + sal_Int32 nSoftBreak = GetTextBreak( rStr, nWidth, nPos, nBreakPos - nPos ); + if (nSoftBreak == -1) + { + nSoftBreak = nPos; + } + SAL_WARN_IF( nSoftBreak >= nBreakPos, "vcl", "Break?!" ); + css::i18n::LineBreakHyphenationOptions aHyphOptions( xHyph, css::uno::Sequence <css::beans::PropertyValue>(), 1 ); + css::i18n::LineBreakUserOptions aUserOptions; + css::i18n::LineBreakResults aLBR = xBI->getLineBreak( rStr, nSoftBreak, rDefLocale, nPos, aHyphOptions, aUserOptions ); + nBreakPos = aLBR.breakIndex; + if ( nBreakPos <= nPos ) + nBreakPos = nSoftBreak; + if ( !bHyphenate ) + return nBreakPos; + + // Whether hyphen or not: Put the word after the hyphen through + // word boundary. + + // nMaxBreakPos the last char that fits into the line + // nBreakPos is the word's start + + // We run into a problem if the doc is so narrow, that a word + // is broken into more than two lines ... + if ( !xHyph.is() ) + return nBreakPos; + + css::i18n::Boundary aBoundary = xBI->getWordBoundary( rStr, nBreakPos, rDefLocale, css::i18n::WordType::DICTIONARY_WORD, true ); + sal_Int32 nWordStart = nPos; + sal_Int32 nWordEnd = aBoundary.endPos; + SAL_WARN_IF(nWordEnd <= nWordStart, "vcl", "Start >= End?"); + + sal_Int32 nWordLen = nWordEnd - nWordStart; + if ( ( nWordEnd < nSoftBreak ) || ( nWordLen <= 3 ) ) + return nBreakPos; + + // #104415# May happen, because getLineBreak may differ from getWordBoundary with DICTIONARY_WORD + // SAL_WARN_IF( nWordEnd < nMaxBreakPos, "vcl", "Hyph: Break?" ); + OUString aWord = rStr.copy( nWordStart, nWordLen ); + sal_Int32 nMinTrail = nWordEnd-nSoftBreak+1; //+1: Before the "broken off" char + css::uno::Reference< css::linguistic2::XHyphenatedWord > xHyphWord; + if (xHyph.is()) + xHyphWord = xHyph->hyphenate( aWord, rDefLocale, aWord.getLength() - nMinTrail, css::uno::Sequence< css::beans::PropertyValue >() ); + if (!xHyphWord.is()) + return nBreakPos; + + bool bAlternate = xHyphWord->isAlternativeSpelling(); + sal_Int32 _nWordLen = 1 + xHyphWord->getHyphenPos(); + + if ( ( _nWordLen < 2 ) || ( (nWordStart+_nWordLen) < 2 ) ) + return nBreakPos; + + if ( bAlternate ) + { + nBreakPos = nWordStart + _nWordLen; + return nBreakPos; + } + + OUString aAlt( xHyphWord->getHyphenatedWord() ); + + // We can have two cases: + // 1) "packen" turns into "pak-ken" + // 2) "Schiffahrt" turns into "Schiff-fahrt" + + // In case 1 we need to replace a char + // In case 2 we add a char + + // Correct recognition is made harder by words such as + // "Schiffahrtsbrennesseln", as the Hyphenator splits all + // positions of the word and comes up with "Schifffahrtsbrennnesseln" + // Thus, we cannot infer the aWord from the AlternativeWord's + // index. + // TODO: The whole junk will be made easier by a function in + // the Hyphenator, as soon as AMA adds it. + sal_Int32 nAltStart = _nWordLen - 1; + sal_Int32 nTxtStart = nAltStart - (aAlt.getLength() - aWord.getLength()); + sal_Int32 nTxtEnd = nTxtStart; + sal_Int32 nAltEnd = nAltStart; + + // The area between nStart and nEnd is the difference + // between AlternativeString and OriginalString + while( nTxtEnd < aWord.getLength() && nAltEnd < aAlt.getLength() && + aWord[nTxtEnd] != aAlt[nAltEnd] ) + { + ++nTxtEnd; + ++nAltEnd; + } + + // If a char was added, we notice it now: + if( nAltEnd > nTxtEnd && nAltStart == nAltEnd && + aWord[ nTxtEnd ] == aAlt[nAltEnd] ) + { + ++nAltEnd; + ++nTxtStart; + ++nTxtEnd; + } + + SAL_INFO_IF( ( nAltEnd - nAltStart ) != 1, "vcl", "Alternate: Wrong assumption!" ); + + sal_Unicode cAlternateReplChar = 0; + if ( nTxtEnd > nTxtStart ) + cAlternateReplChar = aAlt[ nAltStart ]; + + nBreakPos = nWordStart + nTxtStart; + if ( cAlternateReplChar ) + nBreakPos++; + return nBreakPos; + } + + sal_Int32 TextLayoutCommon::BreakLinesSimple(const tools::Long nWidth, OUString const& rStr, + const sal_Int32 nPos, sal_Int32 nBreakPos, tools::Long& nLineWidth) + { + sal_Int32 nSpacePos = rStr.getLength(); + tools::Long nW = 0; + do + { + nSpacePos = rStr.lastIndexOf( ' ', nSpacePos ); + if( nSpacePos != -1 ) + { + if( nSpacePos > nPos ) + nSpacePos--; + nW = GetTextWidth( rStr, nPos, nSpacePos-nPos ); + } + } while( nW > nWidth ); + + if( nSpacePos != -1 ) + { + nBreakPos = nSpacePos; + nLineWidth = GetTextWidth( rStr, nPos, nBreakPos-nPos ); + if( nBreakPos < rStr.getLength()-1 ) + nBreakPos++; + } + return nBreakPos; + } + + tools::Long TextLayoutCommon::GetTextLines(tools::Rectangle const& rRect, const tools::Long nTextHeight, + ImplMultiTextLineInfo& rLineInfo, + tools::Long nWidth, OUString const& rStr, + DrawTextFlags nStyle) + { + SAL_WARN_IF( nWidth <= 0, "vcl", "ImplGetTextLines: nWidth <= 0!" ); + + if ( nWidth <= 0 ) + nWidth = 1; + + rLineInfo.Clear(); + if (rStr.isEmpty()) + return 0; + + const bool bClipping = (nStyle & DrawTextFlags::Clip) && !(nStyle & DrawTextFlags::EndEllipsis); + + tools::Long nMaxLineWidth = 0; + const bool bHyphenate = (nStyle & DrawTextFlags::WordBreakHyphenation) == DrawTextFlags::WordBreakHyphenation; + css::uno::Reference< css::linguistic2::XHyphenator > xHyph; + if (bHyphenate) + { + // get service provider + css::uno::Reference<css::uno::XComponentContext> xContext(comphelper::getProcessComponentContext()); + css::uno::Reference<css::linguistic2::XLinguServiceManager2> xLinguMgr = css::linguistic2::LinguServiceManager::create(xContext); + xHyph = xLinguMgr->getHyphenator(); + } + + css::uno::Reference<css::i18n::XBreakIterator> xBI; + sal_Int32 nPos = 0; + sal_Int32 nLen = rStr.getLength(); + sal_Int32 nCurrentTextY = 0; + while ( nPos < nLen ) + { + sal_Int32 nBreakPos = nPos; + + while ( ( nBreakPos < nLen ) && ( rStr[ nBreakPos ] != '\r' ) && ( rStr[ nBreakPos ] != '\n' ) ) + nBreakPos++; + + tools::Long nLineWidth = GetTextWidth( rStr, nPos, nBreakPos-nPos ); + if ( ( nLineWidth > nWidth ) && ( nStyle & DrawTextFlags::WordBreak ) ) + { + if ( !xBI.is() ) + xBI = vcl::unohelper::CreateBreakIterator(); + + if ( xBI.is() ) + { + nBreakPos = BreakLinesWithIterator(nWidth, rStr, xHyph, xBI, bHyphenate, nPos, nBreakPos); + nLineWidth = GetTextWidth(rStr, nPos, nBreakPos - nPos); + } + else + // fallback to something really simple + nBreakPos = BreakLinesSimple(nWidth, rStr, nPos, nBreakPos, nLineWidth); + } + + if ( nLineWidth > nMaxLineWidth ) + nMaxLineWidth = nLineWidth; + + rLineInfo.AddLine( ImplTextLineInfo( nLineWidth, nPos, nBreakPos-nPos ) ); + + if ( nBreakPos == nPos ) + nBreakPos++; + nPos = nBreakPos; + + if ( nPos < nLen && ( ( rStr[ nPos ] == '\r' ) || ( rStr[ nPos ] == '\n' ) ) ) + { + nPos++; + // CR/LF? + if ( ( nPos < nLen ) && ( rStr[ nPos ] == '\n' ) && ( rStr[ nPos-1 ] == '\r' ) ) + nPos++; + } + nCurrentTextY += nTextHeight; + if (bClipping && nCurrentTextY > rRect.GetHeight()) + break; + } + +#ifdef DBG_UTIL + for ( sal_Int32 nL = 0; nL < rLineInfo.Count(); nL++ ) + { + ImplTextLineInfo& rLine = rLineInfo.GetLine( nL ); + OUString aLine = rStr.copy( rLine.GetIndex(), rLine.GetLen() ); + SAL_WARN_IF( aLine.indexOf( '\r' ) != -1, "vcl", "ImplGetTextLines - Found CR!" ); + SAL_WARN_IF( aLine.indexOf( '\n' ) != -1, "vcl", "ImplGetTextLines - Found LF!" ); + } +#endif + + return nMaxLineWidth; + } + + DefaultTextLayout::~DefaultTextLayout() + { + } + + tools::Long DefaultTextLayout::GetTextWidth( const OUString& _rText, sal_Int32 _nStartIndex, sal_Int32 _nLength ) const + { + return m_rTargetDevice.GetTextWidth( _rText, _nStartIndex, _nLength ); + } + + void DefaultTextLayout::DrawText( const Point& _rStartPoint, const OUString& _rText, sal_Int32 _nStartIndex, + sal_Int32 _nLength, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText ) + { + m_rTargetDevice.DrawText( _rStartPoint, _rText, _nStartIndex, _nLength, _pVector, _pDisplayText ); + } + + tools::Long DefaultTextLayout::GetTextArray( const OUString& _rText, KernArray* _pDXArray, + sal_Int32 _nStartIndex, sal_Int32 _nLength, bool bCaret ) const + { + return m_rTargetDevice.GetTextArray( _rText, _pDXArray, _nStartIndex, _nLength, bCaret ); + } + + sal_Int32 DefaultTextLayout::GetTextBreak( const OUString& _rText, tools::Long _nMaxTextWidth, sal_Int32 _nStartIndex, sal_Int32 _nLength ) const + { + return m_rTargetDevice.GetTextBreak( _rText, _nMaxTextWidth, _nStartIndex, _nLength ); + } + + bool DefaultTextLayout::DecomposeTextRectAction() const + { + return false; + } + + class ReferenceDeviceTextLayout : public TextLayoutCommon + { + public: + ReferenceDeviceTextLayout( const Control& _rControl, OutputDevice& _rTargetDevice, OutputDevice& _rReferenceDevice ); + virtual ~ReferenceDeviceTextLayout(); + + // ITextLayout + virtual tools::Long GetTextWidth( const OUString& rStr, sal_Int32 nIndex, sal_Int32 nLen ) const override; + virtual void DrawText( const Point& _rStartPoint, const OUString& _rText, sal_Int32 _nStartIndex, sal_Int32 _nLength, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText ) override; + virtual tools::Long GetTextArray( const OUString& _rText, KernArray* _pDXAry, sal_Int32 _nStartIndex, sal_Int32 _nLength, bool bCaret = false ) const override; + virtual sal_Int32 GetTextBreak(const OUString& _rText, tools::Long _nMaxTextWidth, sal_Int32 _nStartIndex, sal_Int32 _nLength) const override; + virtual bool DecomposeTextRectAction() const override; + + public: + // equivalents to the respective OutputDevice methods, which take the reference device into account + tools::Rectangle DrawText( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText, const Size* i_pDeviceSize ); + tools::Rectangle GetTextRect( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, Size* o_pDeviceSize ); + + private: + + OutputDevice& m_rTargetDevice; + OutputDevice& m_rReferenceDevice; + const bool m_bRTLEnabled; + + tools::Rectangle m_aCompleteTextRect; + }; + + ReferenceDeviceTextLayout::ReferenceDeviceTextLayout( const Control& _rControl, OutputDevice& _rTargetDevice, + OutputDevice& _rReferenceDevice ) + :m_rTargetDevice( _rTargetDevice ) + ,m_rReferenceDevice( _rReferenceDevice ) + ,m_bRTLEnabled( _rControl.IsRTLEnabled() ) + { + Font const aUnzoomedPointFont( _rControl.GetUnzoomedControlPointFont() ); + const Fraction& aZoom( _rControl.GetZoom() ); + m_rTargetDevice.Push( PushFlags::MAPMODE | PushFlags::FONT | PushFlags::TEXTLAYOUTMODE ); + + MapMode aTargetMapMode( m_rTargetDevice.GetMapMode() ); + SAL_WARN_IF(aTargetMapMode.GetOrigin() != Point(), "vcl", "uhm, the code below won't work here ..."); + + // normally, controls simulate "zoom" by "zooming" the font. This is responsible for (part of) the discrepancies + // between text in Writer and text in controls in Writer, though both have the same font. + // So, if we have a zoom set at the control, then we do not scale the font, but instead modify the map mode + // to accommodate for the zoom. + aTargetMapMode.SetScaleX( aZoom ); // TODO: shouldn't this be "current_scale * zoom"? + aTargetMapMode.SetScaleY( aZoom ); + + // also, use a higher-resolution map unit than "pixels", which should save us some rounding errors when + // translating coordinates between the reference device and the target device. + SAL_WARN_IF(aTargetMapMode.GetMapUnit() != MapUnit::MapPixel, "vcl", "this class is not expected to work with such target devices!"); + // we *could* adjust all the code in this class to handle this case, but at the moment, it's not necessary + const MapUnit eTargetMapUnit = m_rReferenceDevice.GetMapMode().GetMapUnit(); + aTargetMapMode.SetMapUnit( eTargetMapUnit ); + SAL_WARN_IF(aTargetMapMode.GetMapUnit() == MapUnit::MapPixel, "vcl", "a reference device which has map mode PIXEL?!"); + + m_rTargetDevice.SetMapMode( aTargetMapMode ); + + // now that the Zoom is part of the map mode, reset the target device's font to the "unzoomed" version + Font aDrawFont( aUnzoomedPointFont ); + aDrawFont.SetFontSize( OutputDevice::LogicToLogic(aDrawFont.GetFontSize(), MapMode(MapUnit::MapPoint), MapMode(eTargetMapUnit)) ); + _rTargetDevice.SetFont( aDrawFont ); + + // transfer font to the reference device + m_rReferenceDevice.Push( PushFlags::FONT | PushFlags::TEXTLAYOUTMODE ); + Font aRefFont( aUnzoomedPointFont ); + aRefFont.SetFontSize( OutputDevice::LogicToLogic( + aRefFont.GetFontSize(), MapMode(MapUnit::MapPoint), m_rReferenceDevice.GetMapMode()) ); + m_rReferenceDevice.SetFont( aRefFont ); + } + + ReferenceDeviceTextLayout::~ReferenceDeviceTextLayout() + { + m_rReferenceDevice.Pop(); + m_rTargetDevice.Pop(); + } + + namespace + { + bool lcl_normalizeLength( std::u16string_view _rText, const sal_Int32 _nStartIndex, sal_Int32& _io_nLength ) + { + sal_Int32 nTextLength = _rText.size(); + if ( _nStartIndex > nTextLength ) + return false; + if ( _nStartIndex + _io_nLength > nTextLength ) + _io_nLength = nTextLength - _nStartIndex; + return true; + } + } + + tools::Long ReferenceDeviceTextLayout::GetTextArray( const OUString& _rText, KernArray* _pDXAry, sal_Int32 _nStartIndex, sal_Int32 _nLength, bool bCaret ) const + { + if ( !lcl_normalizeLength( _rText, _nStartIndex, _nLength ) ) + return 0; + + // retrieve the character widths from the reference device + tools::Long nTextWidth = m_rReferenceDevice.GetTextArray( _rText, _pDXAry, _nStartIndex, _nLength, bCaret ); +#if OSL_DEBUG_LEVEL > 1 + if ( _pDXAry ) + { + OStringBuffer aTrace; + aTrace.append( "ReferenceDeviceTextLayout::GetTextArray( " ); + aTrace.append( OUStringToOString( _rText, RTL_TEXTENCODING_UTF8 ) ); + aTrace.append( " ): " ); + aTrace.append( nTextWidth ); + aTrace.append( " = ( " ); + for ( sal_Int32 i=0; i<_nLength; ) + { + aTrace.append( _pDXAry->at(i) ); + if ( ++i < _nLength ) + aTrace.append( ", " ); + } + aTrace.append( ")" ); + SAL_INFO( "vcl", aTrace.makeStringAndClear() ); + } +#endif + return nTextWidth; + } + + tools::Long ReferenceDeviceTextLayout::GetTextWidth( const OUString& _rText, sal_Int32 _nStartIndex, sal_Int32 _nLength ) const + { + return GetTextArray( _rText, nullptr, _nStartIndex, _nLength ); + } + + void ReferenceDeviceTextLayout::DrawText( const Point& _rStartPoint, const OUString& _rText, sal_Int32 _nStartIndex, sal_Int32 _nLength, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText ) + { + if ( !lcl_normalizeLength( _rText, _nStartIndex, _nLength ) ) + return; + + if ( _pVector && _pDisplayText ) + { + std::vector< tools::Rectangle > aGlyphBounds; + m_rReferenceDevice.GetGlyphBoundRects( _rStartPoint, _rText, _nStartIndex, _nLength, aGlyphBounds ); + _pVector->insert( _pVector->end(), aGlyphBounds.begin(), aGlyphBounds.end() ); + *_pDisplayText += _rText.subView( _nStartIndex, _nLength ); + return; + } + + KernArray aCharWidths; + tools::Long nTextWidth = GetTextArray( _rText, &aCharWidths, _nStartIndex, _nLength ); + m_rTargetDevice.DrawTextArray( _rStartPoint, _rText, aCharWidths, {}, _nStartIndex, _nLength ); + + m_aCompleteTextRect.Union( tools::Rectangle( _rStartPoint, Size( nTextWidth, m_rTargetDevice.GetTextHeight() ) ) ); + } + + sal_Int32 ReferenceDeviceTextLayout::GetTextBreak( const OUString& _rText, tools::Long _nMaxTextWidth, sal_Int32 _nStartIndex, sal_Int32 _nLength ) const + { + if ( !lcl_normalizeLength( _rText, _nStartIndex, _nLength ) ) + return 0; + + return m_rReferenceDevice.GetTextBreak( _rText, _nMaxTextWidth, _nStartIndex, _nLength ); + } + + bool ReferenceDeviceTextLayout::DecomposeTextRectAction() const + { + return true; + } + + tools::Rectangle ReferenceDeviceTextLayout::DrawText( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, + std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText, const Size* i_pDeviceSize ) + { + if ( _rText.isEmpty() ) + return tools::Rectangle(); + + // determine text layout mode from the RTL-ness of the control whose text we render + text::ComplexTextLayoutFlags nTextLayoutMode = m_bRTLEnabled ? text::ComplexTextLayoutFlags::BiDiRtl : text::ComplexTextLayoutFlags::Default; + m_rReferenceDevice.SetLayoutMode( nTextLayoutMode ); + m_rTargetDevice.SetLayoutMode( nTextLayoutMode | text::ComplexTextLayoutFlags::TextOriginLeft ); + + // ComplexTextLayoutFlags::TextOriginLeft is because when we do actually draw the text (in DrawText( Point, ... )), then + // our caller gives us the left border of the draw position, regardless of script type, text layout, + // and the like in our ctor, we set the map mode of the target device from pixel to twip, but our caller doesn't know this, + // but passed pixel coordinates. So, adjust the rect. + tools::Rectangle aRect( m_rTargetDevice.PixelToLogic( _rRect ) ); + if (i_pDeviceSize) + { + //if i_pDeviceSize is passed in here, it was the original pre logic-to-pixel size of _rRect + SAL_WARN_IF(std::abs(_rRect.GetSize().Width() - m_rTargetDevice.LogicToPixel(*i_pDeviceSize).Width()) > 1, "vcl", "DeviceSize width was expected to match Pixel width"); + SAL_WARN_IF(std::abs(_rRect.GetSize().Height() - m_rTargetDevice.LogicToPixel(*i_pDeviceSize).Height()) > 1, "vcl", "DeviceSize height was expected to match Pixel height"); + aRect.SetSize(*i_pDeviceSize); + } + + m_aCompleteTextRect.SetEmpty(); + m_rTargetDevice.DrawText( aRect, _rText, _nStyle, _pVector, _pDisplayText, this ); + tools::Rectangle aTextRect = m_aCompleteTextRect; + + if ( aTextRect.IsEmpty() && !aRect.IsEmpty() ) + { + // this happens for instance if we're in a PaintToDevice call, where only a MetaFile is recorded, + // but no actual painting happens, so our "DrawText( Point, ... )" is never called + // In this case, calculate the rect from what OutputDevice::GetTextRect would give us. This has + // the disadvantage of less accuracy, compared with the approach to calculate the rect from the + // single "DrawText( Point, ... )" calls, since more intermediate arithmetic will translate + // from ref- to target-units. + aTextRect = m_rTargetDevice.GetTextRect( aRect, _rText, _nStyle, nullptr, this ); + } + + // similar to above, the text rect now contains TWIPs (or whatever unit the ref device has), but the caller + // expects pixel coordinates + aTextRect = m_rTargetDevice.LogicToPixel( aTextRect ); + + // convert the metric vector + if ( _pVector ) + { + for ( auto& rCharRect : *_pVector ) + { + rCharRect = m_rTargetDevice.LogicToPixel( rCharRect ); + } + } + + return aTextRect; + } + + tools::Rectangle ReferenceDeviceTextLayout::GetTextRect( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, Size* o_pDeviceSize ) + { + if ( _rText.isEmpty() ) + return tools::Rectangle(); + + // determine text layout mode from the RTL-ness of the control whose text we render + text::ComplexTextLayoutFlags nTextLayoutMode = m_bRTLEnabled ? text::ComplexTextLayoutFlags::BiDiRtl : text::ComplexTextLayoutFlags::Default; + m_rReferenceDevice.SetLayoutMode( nTextLayoutMode ); + m_rTargetDevice.SetLayoutMode( nTextLayoutMode | text::ComplexTextLayoutFlags::TextOriginLeft ); + + // ComplexTextLayoutFlags::TextOriginLeft is because when we do actually draw the text (in DrawText( Point, ... )), then + // our caller gives us the left border of the draw position, regardless of script type, text layout, + // and the like in our ctor, we set the map mode of the target device from pixel to twip, but our caller doesn't know this, + // but passed pixel coordinates. So, adjust the rect. + tools::Rectangle aRect( m_rTargetDevice.PixelToLogic( _rRect ) ); + + tools::Rectangle aTextRect = m_rTargetDevice.GetTextRect( aRect, _rText, _nStyle, nullptr, this ); + + //if o_pDeviceSize is available, stash the pre logic-to-pixel size in it + if (o_pDeviceSize) + { + *o_pDeviceSize = aTextRect.GetSize(); + } + + // similar to above, the text rect now contains TWIPs (or whatever unit the ref device has), but the caller + // expects pixel coordinates + aTextRect = m_rTargetDevice.LogicToPixel( aTextRect ); + + return aTextRect; + } + + ControlTextRenderer::ControlTextRenderer( const Control& _rControl, OutputDevice& _rTargetDevice, OutputDevice& _rReferenceDevice ) + :m_pImpl( new ReferenceDeviceTextLayout( _rControl, _rTargetDevice, _rReferenceDevice ) ) + { + } + + ControlTextRenderer::~ControlTextRenderer() + { + } + + tools::Rectangle ControlTextRenderer::DrawText( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, + std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText, const Size* i_pDeviceSize ) + { + return m_pImpl->DrawText( _rRect, _rText, _nStyle, _pVector, _pDisplayText, i_pDeviceSize ); + } + + tools::Rectangle ControlTextRenderer::GetTextRect( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, Size* o_pDeviceSize = nullptr ) + { + return m_pImpl->GetTextRect( _rRect, _rText, _nStyle, o_pDeviceSize ); + } + +} // namespace vcl + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |