blob: 68d3c18b3e28d6bbd1a00fe77741527b105940d2 [file] [log] [blame]
/**************************************************************
*
* 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
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*************************************************************/
#include "ctfonts.hxx"
// =======================================================================
class CTLayout
: public SalLayout
{
public:
explicit CTLayout( const CTTextStyle* );
virtual ~CTLayout( void );
virtual bool LayoutText( ImplLayoutArgs& );
virtual void AdjustLayout( ImplLayoutArgs& );
virtual void DrawText( SalGraphics& ) const;
virtual int GetNextGlyphs( int nLen, sal_GlyphId* pOutGlyphIds, Point& rPos, int&,
sal_Int32* pGlyphAdvances, int* pCharIndexes ) const;
virtual long GetTextWidth() const;
virtual long FillDXArray( sal_Int32* pDXArray ) const;
virtual int GetTextBreak( long nMaxWidth, long nCharExtra, int nFactor ) const;
virtual void GetCaretPositions( int nArraySize, sal_Int32* pCaretXArray ) const;
virtual bool GetGlyphOutlines( SalGraphics&, PolyPolyVector& ) const;
virtual bool GetBoundRect( SalGraphics&, Rectangle& ) const;
const ImplFontData* GetFallbackFontData( sal_GlyphId ) const;
virtual void InitFont( void) const;
virtual void MoveGlyph( int nStart, long nNewXPos );
virtual void DropGlyph( int nStart );
virtual void Simplify( bool bIsBase );
private:
// CoreText specific objects
CFMutableDictionaryRef mpStyleDict;
CFAttributedStringRef mpAttrString;
CTLineRef mpCTLine;
int mnCharCount; // ==mnEndCharPos-mnMinCharPos
int mnTrailingSpaceCount;
double mfTrailingSpaceWidth; // preserves the width of stripped-off trailing space
// to prevent overflows
// font requests get size limited by downscaling huge fonts
// in these cases the font scale becomes something bigger than 1.0
float mfFontScale; // TODO: does CoreText have a font size limit?
CGFloat mfFontRotation; // text direction angle (in radians)
CGFloat mfFontStretch; // <1.0: font gets squeezed, >1.0: font gets stretched
// cached details about the resulting layout
// mutable members since these details are all lazy initialized
mutable double mfCachedWidth; // cached value of resulting typographical width
// x-offset relative to layout origin
// currently only used in RTL-layouts
mutable long mnBaseAdv;
};
// =======================================================================
CTLayout::CTLayout( const CTTextStyle* pTextStyle )
: mpStyleDict( pTextStyle->GetStyleDict() )
, mpAttrString( NULL )
, mpCTLine( NULL )
, mnCharCount( 0 )
, mnTrailingSpaceCount( 0 )
, mfTrailingSpaceWidth( 0.0 )
, mfFontScale( pTextStyle->mfFontScale )
, mfFontRotation( pTextStyle->mfFontRotation )
, mfFontStretch( pTextStyle->mfFontStretch )
, mfCachedWidth( -1 )
, mnBaseAdv( 0 )
{
CFRetain( mpStyleDict );
}
// -----------------------------------------------------------------------
CTLayout::~CTLayout()
{
if( mpCTLine )
CFRelease( mpCTLine );
if( mpAttrString )
CFRelease( mpAttrString );
CFRelease( mpStyleDict );
}
// -----------------------------------------------------------------------
bool CTLayout::LayoutText( ImplLayoutArgs& rArgs )
{
// release an eventual older layout
if( mpAttrString )
CFRelease( mpAttrString );
mpAttrString = NULL;
if( mpCTLine )
CFRelease( mpCTLine );
mpCTLine = NULL;
// initialize the new layout
SalLayout::AdjustLayout( rArgs );
mnCharCount = mnEndCharPos - mnMinCharPos;
// short circuit if there is nothing to do
if( mnCharCount <= 0 )
return false;
// prepare the string to be layouted by CoreText
CFStringRef aCFText = CFStringCreateWithCharactersNoCopy( NULL, rArgs.mpStr + mnMinCharPos, mnCharCount, kCFAllocatorNull );
// #i124375# force soft-hyphen visibility to meet the expectations of Writer+EditEngine
if( CFStringFind( aCFText, (CFStringRef)@"\u00AD", 0).length > 0 )
{
NSString* pDashStr = [(NSString*)aCFText stringByReplacingOccurrencesOfString: @"\u00AD" withString: @"-"];
aCFText = CFStringCreateCopy( NULL, (CFStringRef)pDashStr );
}
// create the CoreText line layout using the requested text style
mpAttrString = CFAttributedStringCreate( NULL, aCFText, mpStyleDict );
mpCTLine = CTLineCreateWithAttributedString( mpAttrString );
CFRelease( aCFText);
// get info about trailing whitespace to prepare for text justification in AdjustLayout()
mnTrailingSpaceCount = 0;
for( int i = mnEndCharPos; --i >= mnMinCharPos; ++mnTrailingSpaceCount )
if( !IsSpacingGlyph( rArgs.mpStr[i] | GF_ISCHAR )
&& (rArgs.mpStr[i] != 0x00A0) )
break;
return true;
}
// -----------------------------------------------------------------------
void CTLayout::AdjustLayout( ImplLayoutArgs& rArgs )
{
if( !mpCTLine)
return;
int nPixelWidth = rArgs.mnLayoutWidth;
if( rArgs.mpDXArray )
{
// for now we are only interested in the layout width
// TODO: use all mpDXArray elements for layouting
nPixelWidth = rArgs.mpDXArray[ mnCharCount-1 ];
}
else if( !nPixelWidth ) // short-circuit if there is nothing to adjust
return;
// short-circuit when justifying an all-whitespace string
if( mnTrailingSpaceCount >= mnCharCount)
{
mfCachedWidth = nPixelWidth / mfFontScale;
return;
}
// return early if there is nothing to do
if( nPixelWidth <= 0 )
return;
// HACK: justification requests which change the width by just one pixel are probably
// #i86038# introduced by lossy conversions between integer based coordinate system
const int nOrigWidth = GetTextWidth();
if( (nOrigWidth >= nPixelWidth-1) && (nOrigWidth <= nPixelWidth+1) )
return;
// if the text to be justified has whitespace in it then
// - Writer goes crazy with its HalfSpace magic
// - CoreText handles spaces specially (in particular at the text end)
if( mnTrailingSpaceCount ) {
int nTrailingSpaceWidth = 0;
if( rArgs.mpDXArray) {
const int nFullPixWidth = nPixelWidth;
nPixelWidth = rArgs.mpDXArray[ mnCharCount-1-mnTrailingSpaceCount ];
nTrailingSpaceWidth = nFullPixWidth - nPixelWidth;
mfTrailingSpaceWidth = nTrailingSpaceWidth;
} else {
if( mfTrailingSpaceWidth <= 0.0 )
mfTrailingSpaceWidth = CTLineGetTrailingWhitespaceWidth( mpCTLine );
nTrailingSpaceWidth = rint( mfTrailingSpaceWidth );
nPixelWidth -= nTrailingSpaceWidth;
}
if( nPixelWidth <= 0 )
return;
// recreate the CoreText line layout without trailing spaces
CFRelease( mpCTLine );
CFStringRef aCFText = CFStringCreateWithCharactersNoCopy( NULL, rArgs.mpStr + mnMinCharPos,
mnCharCount - mnTrailingSpaceCount, kCFAllocatorNull );
CFAttributedStringRef pAttrStr = CFAttributedStringCreate( NULL, aCFText, mpStyleDict );
mpCTLine = CTLineCreateWithAttributedString( pAttrStr );
CFRelease( aCFText);
CFRelease( pAttrStr );
// in RTL-layouts trailing spaces are leftmost
// TODO: use BiDi-algorithm to thoroughly check this assumption
if( rArgs.mnFlags & SAL_LAYOUT_BIDI_RTL)
mnBaseAdv = nTrailingSpaceWidth;
}
const double fAdjustedWidth = nPixelWidth / mfFontScale;
CTLineRef pNewCTLine = CTLineCreateJustifiedLine( mpCTLine, 1.0, fAdjustedWidth );
if( !pNewCTLine ) { // CTLineCreateJustifiedLine can and does fail
// handle failure by keeping the unjustified layout
// TODO: a better solution such as
// - forcing glyph overlap
// - changing the font size
// - changing the CTM matrix
return;
}
CFRelease( mpCTLine );
mpCTLine = pNewCTLine;
mfCachedWidth = fAdjustedWidth + mfTrailingSpaceWidth;
}
// -----------------------------------------------------------------------
void CTLayout::DrawText( SalGraphics& rGraphics ) const
{
AquaSalGraphics& rAquaGraphics = static_cast<AquaSalGraphics&>(rGraphics);
// short circuit if there is nothing to do
if( (mnCharCount <= 0)
|| !rAquaGraphics.CheckContext() )
return;
// the view is vertically flipped => flipped glyphs
// so apply a temporary transformation that it flips back
// also compensate if the font was size limited
CGContextSaveGState( rAquaGraphics.mrContext );
CGContextScaleCTM( rAquaGraphics.mrContext, +mfFontScale, -mfFontScale );
CGContextSetShouldAntialias( rAquaGraphics.mrContext, !rAquaGraphics.mbNonAntialiasedText );
// set the text transformation (e.g. position)
const Point aVclPos = GetDrawPosition( Point(mnBaseAdv,0) );
CGPoint aTextPos = { +aVclPos.X()/mfFontScale, -aVclPos.Y()/mfFontScale };
if( mfFontRotation != 0.0 )
{
CGContextRotateCTM( rAquaGraphics.mrContext, +mfFontRotation );
const CGAffineTransform aInvMatrix = CGAffineTransformMakeRotation( -mfFontRotation );
aTextPos = CGPointApplyAffineTransform( aTextPos, aInvMatrix );
}
CGContextSetTextPosition( rAquaGraphics.mrContext, aTextPos.x, aTextPos.y );
// request an update of the to-be-changed window area
if( rAquaGraphics.IsWindowGraphics() )
{
const CGRect aInkRect = CTLineGetImageBounds( mpCTLine, rAquaGraphics.mrContext );
const CGRect aRefreshRect = CGContextConvertRectToDeviceSpace( rAquaGraphics.mrContext, aInkRect );
rAquaGraphics.RefreshRect( aRefreshRect );
}
// set the text color as fill color (see kCTForegroundColorFromContextAttributeName)
CGContextSetFillColor( rAquaGraphics.mrContext, rAquaGraphics.maTextColor.AsArray() );
// draw the text
CTLineDraw( mpCTLine, rAquaGraphics.mrContext );
// restore the original graphic context transformations
CGContextRestoreGState( rAquaGraphics.mrContext );
}
// -----------------------------------------------------------------------
int CTLayout::GetNextGlyphs( int nLen, sal_GlyphId* pOutGlyphIds, Point& rPos, int& nStart,
sal_Int32* pGlyphAdvances, int* pCharIndexes ) const
{
if( !mpCTLine )
return 0;
if( nStart < 0 ) // first glyph requested?
nStart = 0;
nLen = 1; // TODO: handle nLen>1 below
// prepare to iterate over the glyph runs
int nCount = 0;
int nSubIndex = nStart;
const DynCoreTextSyms& rCT = DynCoreTextSyms::get();
typedef std::vector<CGGlyph> CGGlyphVector;
typedef std::vector<CGPoint> CGPointVector;
typedef std::vector<CGSize> CGSizeVector;
typedef std::vector<CFIndex> CFIndexVector;
CGGlyphVector aCGGlyphVec;
CGPointVector aCGPointVec;
CGSizeVector aCGSizeVec;
CFIndexVector aCFIndexVec;
// TODO: iterate over cached layout
CFArrayRef aGlyphRuns = rCT.LineGetGlyphRuns( mpCTLine );
const int nRunCount = CFArrayGetCount( aGlyphRuns );
for( int nRunIndex = 0; nRunIndex < nRunCount; ++nRunIndex ) {
CTRunRef pGlyphRun = (CTRunRef)CFArrayGetValueAtIndex( aGlyphRuns, nRunIndex );
const CFIndex nGlyphsInRun = rCT.RunGetGlyphCount( pGlyphRun );
// skip to the first glyph run of interest
if( nSubIndex >= nGlyphsInRun ) {
nSubIndex -= nGlyphsInRun;
continue;
}
const CFRange aFullRange = CFRangeMake( 0, nGlyphsInRun );
// get glyph run details
const CGGlyph* pCGGlyphIdx = rCT.RunGetGlyphsPtr( pGlyphRun );
if( !pCGGlyphIdx ) {
aCGGlyphVec.reserve( nGlyphsInRun );
CTRunGetGlyphs( pGlyphRun, aFullRange, &aCGGlyphVec[0] );
pCGGlyphIdx = &aCGGlyphVec[0];
}
const CGPoint* pCGGlyphPos = rCT.RunGetPositionsPtr( pGlyphRun );
if( !pCGGlyphPos ) {
aCGPointVec.reserve( nGlyphsInRun );
CTRunGetPositions( pGlyphRun, aFullRange, &aCGPointVec[0] );
pCGGlyphPos = &aCGPointVec[0];
}
const CGSize* pCGGlyphAdvs = NULL;
if( pGlyphAdvances) {
pCGGlyphAdvs = rCT.RunGetAdvancesPtr( pGlyphRun );
if( !pCGGlyphAdvs) {
aCGSizeVec.reserve( nGlyphsInRun );
CTRunGetAdvances( pGlyphRun, aFullRange, &aCGSizeVec[0] );
pCGGlyphAdvs = &aCGSizeVec[0];
}
}
const CFIndex* pCGGlyphStrIdx = NULL;
if( pCharIndexes) {
pCGGlyphStrIdx = rCT.RunGetStringIndicesPtr( pGlyphRun );
if( !pCGGlyphStrIdx) {
aCFIndexVec.reserve( nGlyphsInRun );
CTRunGetStringIndices( pGlyphRun, aFullRange, &aCFIndexVec[0] );
pCGGlyphStrIdx = &aCFIndexVec[0];
}
}
// get the details for each interesting glyph
// TODO: handle nLen>1
for(; (--nLen >= 0) && (nSubIndex < nGlyphsInRun); ++nSubIndex, ++nStart )
{
// convert glyph details for VCL
*(pOutGlyphIds++) = pCGGlyphIdx[ nSubIndex ];
if( pGlyphAdvances )
*(pGlyphAdvances++) = mfFontStretch * pCGGlyphAdvs[ nSubIndex ].width;
if( pCharIndexes )
*(pCharIndexes++) = pCGGlyphStrIdx[ nSubIndex] + mnMinCharPos;
if( !nCount++ ) {
const CGPoint& rCurPos = pCGGlyphPos[ nSubIndex ];
rPos = GetDrawPosition( Point( mfFontScale * mfFontStretch * rCurPos.x, mfFontScale * rCurPos.y) );
}
}
nSubIndex = 0; // prepare for the next glyph run
break; // TODO: handle nLen>1
}
return nCount;
}
// -----------------------------------------------------------------------
long CTLayout::GetTextWidth() const
{
if( (mnCharCount <= 0) || !mpCTLine )
return 0;
if( mfCachedWidth < 0.0 )
mfCachedWidth = CTLineGetTypographicBounds( mpCTLine, NULL, NULL, NULL );
const long nScaledWidth = lrint( mfFontScale * mfCachedWidth );
return nScaledWidth;
}
// -----------------------------------------------------------------------
long CTLayout::FillDXArray( sal_Int32* pDXArray ) const
{
// short circuit requests which don't need full details
if( !pDXArray )
return GetTextWidth();
long nPixWidth = GetTextWidth();
if( pDXArray ) {
// prepare the sub-pixel accurate logical-width array
::std::vector<float> aWidthVector( mnCharCount );
if( mnTrailingSpaceCount && (mfTrailingSpaceWidth > 0.0) ) {
const double fOneWidth = mfTrailingSpaceWidth / mnTrailingSpaceCount;
for( int i = 1; i <= mnTrailingSpaceCount; ++i)
aWidthVector[ mnCharCount - i ] = fOneWidth;
}
// measure advances in each glyph run
CFArrayRef aGlyphRuns = CTLineGetGlyphRuns( mpCTLine );
const int nRunCount = CFArrayGetCount( aGlyphRuns );
typedef std::vector<CGSize> CGSizeVector;
CGSizeVector aSizeVec;
typedef std::vector<CFIndex> CFIndexVector;
CFIndexVector aIndexVec;
for( int nRunIndex = 0; nRunIndex < nRunCount; ++nRunIndex ) {
CTRunRef pGlyphRun = (CTRunRef)CFArrayGetValueAtIndex( aGlyphRuns, nRunIndex );
const CFIndex nGlyphCount = CTRunGetGlyphCount( pGlyphRun );
const CFRange aFullRange = CFRangeMake( 0, nGlyphCount );
aSizeVec.resize( nGlyphCount );
aIndexVec.resize( nGlyphCount );
CTRunGetAdvances( pGlyphRun, aFullRange, &aSizeVec[0] );
CTRunGetStringIndices( pGlyphRun, aFullRange, &aIndexVec[0] );
for( int i = 0; i != nGlyphCount; ++i ) {
const int nRelIdx = aIndexVec[i];
aWidthVector[nRelIdx] += aSizeVec[i].width;
}
}
// convert the sub-pixel accurate array into classic pDXArray integers
float fWidthSum = 0.0;
sal_Int32 nOldDX = 0;
for( int i = 0; i < mnCharCount; ++i) {
const sal_Int32 nNewDX = rint( fWidthSum += aWidthVector[i]);
pDXArray[i] = nNewDX - nOldDX;
nOldDX = nNewDX;
}
}
return nPixWidth;
}
// -----------------------------------------------------------------------
int CTLayout::GetTextBreak( long nMaxWidth, long nCharExtra, int nFactor ) const
{
if( !mpCTLine )
return STRING_LEN;
CTTypesetterRef aCTTypeSetter = CTTypesetterCreateWithAttributedString( mpAttrString );
CFIndex nBestGuess = (nCharExtra >= 0) ? 0 : mnCharCount;
for( int i = 1; i <= mnCharCount; i *= 2 )
{
// guess the target width considering char-extra expansion/condensation
const long nTargetWidth = nMaxWidth - nBestGuess * nCharExtra;
const double fCTMaxWidth = nTargetWidth / (nFactor * mfFontScale);
// calculate the breaking index for the guessed target width
const CFIndex nNewIndex = CTTypesetterSuggestClusterBreak( aCTTypeSetter, 0, fCTMaxWidth );
if( nNewIndex >= mnCharCount ) {
CFRelease( aCTTypeSetter );
return STRING_LEN;
}
// check if the original extra-width guess was good
if( !nCharExtra )
nBestGuess = nNewIndex;
if( nBestGuess == nNewIndex )
break;
// prepare another round for a different number of characters
CFIndex nNewGuess = (nNewIndex + nBestGuess + 1) / 2;
if( nNewGuess == nBestGuess )
nNewGuess += (nNewIndex > nBestGuess) ? +1 : -1;
nBestGuess = nNewGuess;
}
// suggest the best fitting cluster break as breaking position
CFRelease( aCTTypeSetter );
const int nIndex = nBestGuess + mnMinCharPos;
return nIndex;
}
// -----------------------------------------------------------------------
void CTLayout::GetCaretPositions( int nMaxIndex, sal_Int32* pCaretXArray ) const
{
DBG_ASSERT( ((nMaxIndex>0)&&!(nMaxIndex&1)),
"CTLayout::GetCaretPositions() : invalid number of caret pairs requested");
// initialize the caret positions
for( int i = 0; i < nMaxIndex; ++i )
pCaretXArray[ i ] = -1;
const DynCoreTextSyms& rCT = DynCoreTextSyms::get();
for( int n = 0; n <= mnCharCount; ++n )
{
// measure the characters cursor position
CGFloat fPos2 = -1;
const CGFloat fPos1 = rCT.LineGetOffsetForStringIndex( mpCTLine, n, &fPos2 );
(void)fPos2; // TODO: split cursor at line direction change
// update previous trailing position
if( n > 0 )
pCaretXArray[ 2*n-1 ] = lrint( fPos1 * mfFontScale );
// update current leading position
if( 2*n >= nMaxIndex )
break;
pCaretXArray[ 2*n+0 ] = lrint( fPos1 * mfFontScale );
}
}
// -----------------------------------------------------------------------
bool CTLayout::GetBoundRect( SalGraphics& rGraphics, Rectangle& rVCLRect ) const
{
AquaSalGraphics& rAquaGraphics = static_cast<AquaSalGraphics&>(rGraphics);
CGRect aMacRect = CTLineGetImageBounds( mpCTLine, rAquaGraphics.mrContext );
CGPoint aMacPos = CGContextGetTextPosition( rAquaGraphics.mrContext );
aMacRect.origin.x -= aMacPos.x;
aMacRect.origin.y -= aMacPos.y;
const Point aPos = GetDrawPosition( Point(mnBaseAdv, 0) );
// CoreText top-bottom are vertically flipped from a VCL aspect
rVCLRect.Left() = aPos.X() + mfFontScale * aMacRect.origin.x;
rVCLRect.Right() = aPos.X() + mfFontScale * (aMacRect.origin.x + aMacRect.size.width);
rVCLRect.Bottom() = aPos.Y() - mfFontScale * aMacRect.origin.y;
rVCLRect.Top() = aPos.Y() - mfFontScale * (aMacRect.origin.y + aMacRect.size.height);
return true;
}
// =======================================================================
// glyph fallback is supported directly by Aqua
// so methods used only by MultiSalLayout can be dummy implementated
bool CTLayout::GetGlyphOutlines( SalGraphics&, PolyPolyVector& ) const { return false; }
void CTLayout::InitFont() const {}
void CTLayout::MoveGlyph( int /*nStart*/, long /*nNewXPos*/ ) {}
void CTLayout::DropGlyph( int /*nStart*/ ) {}
void CTLayout::Simplify( bool /*bIsBase*/ ) {}
// get the ImplFontData for a glyph fallback font
// for a glyphid that was returned by CTLayout::GetNextGlyphs()
const ImplFontData* CTLayout::GetFallbackFontData( sal_GlyphId /*aGlyphId*/ ) const
{
#if 0
// check if any fallback fonts were needed
if( !mpFallbackInfo )
return NULL;
// check if the current glyph needs a fallback font
int nFallbackLevel = (aGlyphId & GF_FONTMASK) >> GF_FONTSHIFT;
if( !nFallbackLevel )
return NULL;
pFallbackFont = mpFallbackInfo->GetFallbackFontData( nFallbackLevel );
#else
// let CoreText's font cascading handle glyph fallback
const ImplFontData* pFallbackFont = NULL;
#endif
return pFallbackFont;
}
// =======================================================================
SalLayout* CTTextStyle::GetTextLayout( void ) const
{
return new CTLayout( this);
}
// =======================================================================