mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-25 09:35:36 +00:00
When text attachments (such as tables or wide images) exceeded container width,
`CPTypesetter` correctly forced a line break following the attachment but
erroneously reverted the line's height and baseline to pre-attachment metrics.
Consequently, oversized attachments rendered on the baseline of preceding text,
causing them to overlap and obscure content above them in narrowed viewports.
* AppKit/CPTextView/CPTypesetter.j:
- Update line layout logic to incorporate attachment dimensions into line height
and baseline calculations when line breaks encompass overflowing attachments.
* Tests/Manual/TextEditor/WORKLIST.md:
- Mark wide/overflowing table layout tests as completed.
Attachments and tables wider than the text container now correctly occupy their own distinct lines without overlapping preceding content.
697 lines
26 KiB
Plaintext
697 lines
26 KiB
Plaintext
/*
|
|
* CPTypesetter.j
|
|
* AppKit
|
|
*
|
|
* Created by Daniel Boehringer on 27/12/2013.
|
|
* All modifications copyright Daniel Boehringer 2013.
|
|
* Extensive code formatting and review by Andrew Hankinson
|
|
* Based on original work by
|
|
* Created by Emmanuel Maillard on 27/02/2010.
|
|
* Copyright Emmanuel Maillard 2010.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
@import <Foundation/CPObject.j>
|
|
|
|
@import "CPParagraphStyle.j"
|
|
@import "CPTextStorage.j"
|
|
@import "CPFont.j"
|
|
|
|
@global CPBaselineOffsetAttributeName
|
|
@global CPSuperscriptAttributeName
|
|
|
|
// forward declare these classes for type matching
|
|
@class CPLayoutManager
|
|
@class CPTextContainer
|
|
@class CPTextView
|
|
|
|
/*
|
|
CPTypesetterControlCharacterAction
|
|
*/
|
|
CPTypesetterZeroAdvancementAction = 1 << 0;
|
|
CPTypesetterWhitespaceAction = 1 << 1;
|
|
CPSTypesetterHorizontalTabAction = 1 << 2;
|
|
CPTypesetterLineBreakAction = 1 << 3;
|
|
CPTypesetterParagraphBreakAction = 1 << 4;
|
|
CPTypesetterContainerBreakAction = 1 << 5;
|
|
|
|
var CPSystemTypesetterFactory,
|
|
_sharedSimpleTypesetter,
|
|
_CPTypesetterCachedDecimalSeparator = nil;
|
|
|
|
/*
|
|
Returns the decimal separator of the user's locale, which a decimal tab stop aligns on.
|
|
*/
|
|
var _CPTypesetterDecimalSeparator = function()
|
|
{
|
|
if (!_CPTypesetterCachedDecimalSeparator)
|
|
{
|
|
var formatted = (1.5).toLocaleString();
|
|
|
|
_CPTypesetterCachedDecimalSeparator = formatted.length === 3 ? formatted.charAt(1) : @".";
|
|
}
|
|
|
|
return _CPTypesetterCachedDecimalSeparator;
|
|
};
|
|
|
|
@implementation CPTypesetter : CPObject
|
|
|
|
|
|
// MARK: -
|
|
// MARK: Class methods
|
|
|
|
+ (void)initialize
|
|
{
|
|
[CPTypesetter _setSystemTypesetterFactory:[CPSimpleTypesetter class]];
|
|
}
|
|
|
|
+ (id)sharedSystemTypesetter
|
|
{
|
|
return [CPSystemTypesetterFactory sharedInstance];
|
|
}
|
|
|
|
+ (void)_setSystemTypesetterFactory:(Class)aClass
|
|
{
|
|
CPSystemTypesetterFactory = aClass;
|
|
}
|
|
|
|
- (CPTypesetterControlCharacterAction)actionForControlCharacterAtIndex:(unsigned)charIndex
|
|
{
|
|
return CPTypesetterZeroAdvancementAction;
|
|
}
|
|
|
|
- (CPLayoutManager)layoutManager
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (CPTextContainer)currentTextContainer
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (CPArray)textContainers
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (void)layoutGlyphsInLayoutManager:(CPLayoutManager)layoutManager
|
|
startingAtGlyphIndex:(unsigned)startGlyphIndex
|
|
maxNumberOfLineFragments:(unsigned)maxNumLines
|
|
nextGlyphIndex:(UIntegerReference)nextGlyph
|
|
{
|
|
_CPRaiseInvalidAbstractInvocation(self, _cmd);
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation CPSimpleTypesetter : CPTypesetter
|
|
{
|
|
CPLayoutManager _layoutManager @accessors(property=layoutManager);
|
|
CPTextContainer _currentTextContainer @accessors(property=currentTextContainer);
|
|
CPTextStorage _textStorage;
|
|
|
|
CPRange _attributesRange;
|
|
CPDictionary _currentAttributes;
|
|
CPParagraphStyle _currentParagraph;
|
|
|
|
float _lineHeight;
|
|
float _lineBase;
|
|
float _lineWidth;
|
|
|
|
unsigned _indexOfCurrentContainer;
|
|
|
|
CPArray _lineFragments;
|
|
}
|
|
|
|
|
|
// MARK: -
|
|
// MARK: Class methods
|
|
|
|
+ (id)sharedInstance
|
|
{
|
|
if (!_sharedSimpleTypesetter)
|
|
_sharedSimpleTypesetter = [[CPSimpleTypesetter alloc] init];
|
|
|
|
return _sharedSimpleTypesetter;
|
|
}
|
|
|
|
- (CPArray)textContainers
|
|
{
|
|
return [_layoutManager textContainers];
|
|
}
|
|
|
|
// Retrieves correct CPTextTab stop accounting for custom stops and default intervals
|
|
- (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction
|
|
{
|
|
var tabStops = [_currentParagraph tabStops],
|
|
defaultInterval = [_currentParagraph defaultTabInterval] || 28.0;
|
|
|
|
var l = tabStops ? [tabStops count] : 0;
|
|
|
|
// 1. If custom tab stops exist ahead of current position, use the first one encountered
|
|
if (l > 0)
|
|
{
|
|
for (var i = 0; i < l; i++)
|
|
{
|
|
var tab = [tabStops objectAtIndex:i];
|
|
|
|
if ([tab location] > aWidth)
|
|
return tab;
|
|
}
|
|
}
|
|
|
|
// 2. Otherwise (or when all custom tab stops are behind the text), advance to the next default interval
|
|
var nextLocation = (Math.floor(aWidth / defaultInterval) + 1) * defaultInterval;
|
|
|
|
return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation];
|
|
}
|
|
|
|
- (BOOL)_flushRange:(CPRange)lineRange
|
|
lineOrigin:(CGPoint)lineOrigin
|
|
currentContainer:(CPTextContainer)aContainer
|
|
advancements:(CPArray)advancements
|
|
lineCount:(unsigned)lineCount
|
|
sameLine:(BOOL)sameLine
|
|
wordWrapped:(BOOL)wordWrapped
|
|
rightMargin:(float)rightMargin
|
|
{
|
|
// Line positions are computed inside the line fragment padding. The padding is added here, where
|
|
// the line fragment rectangle is stored, and alignment uses the width inside the padding.
|
|
var padding = [aContainer lineFragmentPadding],
|
|
myX = 0,
|
|
rect = CGRectMake(lineOrigin.x + padding, lineOrigin.y, _lineWidth, _lineHeight);
|
|
|
|
// As in Cocoa, text is aligned between the indents, and whitespace at the end of the line hangs
|
|
// outside the alignment: it does not count in the line's visible width.
|
|
var theString = [_textStorage string],
|
|
availableWidth = rightMargin - lineOrigin.x,
|
|
visibleLength = lineRange.length,
|
|
visibleWidth = _lineWidth;
|
|
|
|
while (visibleLength > 0)
|
|
{
|
|
var trailingCharCode = theString.charCodeAt(lineRange.location + visibleLength - 1);
|
|
|
|
if (trailingCharCode !== 32 && trailingCharCode !== 10 && trailingCharCode !== 13)
|
|
break;
|
|
|
|
visibleLength--;
|
|
visibleWidth -= advancements[visibleLength].width;
|
|
}
|
|
|
|
[_layoutManager _appendNewLineFragmentInTextContainer:_currentTextContainer forGlyphRange:lineRange];
|
|
|
|
var fragment = [_layoutManager._lineFragments lastObject];
|
|
fragment._isLast = !sameLine;
|
|
_lineFragments.push(fragment);
|
|
|
|
[_layoutManager setLineFragmentRect:rect forGlyphRange:lineRange usedRect:rect];
|
|
|
|
switch ([_currentParagraph alignment])
|
|
{
|
|
case CPLeftTextAlignment:
|
|
myX = 0;
|
|
break;
|
|
|
|
case CPCenterTextAlignment:
|
|
myX = (availableWidth - visibleWidth) / 2;
|
|
break;
|
|
|
|
case CPRightTextAlignment:
|
|
myX = availableWidth - visibleWidth;
|
|
break;
|
|
|
|
case CPJustifiedTextAlignment:
|
|
// Only a line that wraps is justified. The last line of a paragraph, and the text
|
|
// before a tab on the same line, stay left aligned, as in Cocoa. The spare width is
|
|
// spread evenly over the spaces inside the visible part of the line. The line fragment
|
|
// draws its spans with the same extra word spacing.
|
|
if (wordWrapped && !sameLine)
|
|
{
|
|
var spaceIndexes = [];
|
|
|
|
for (var i = 0; i < visibleLength; i++)
|
|
{
|
|
var charCode = theString.charCodeAt(lineRange.location + i);
|
|
|
|
if (charCode === 32 || charCode === 160)
|
|
spaceIndexes.push(i);
|
|
}
|
|
|
|
var spaceCount = spaceIndexes.length,
|
|
extraWidth = spaceCount ? (availableWidth - visibleWidth) / spaceCount : 0.0;
|
|
|
|
if (extraWidth > 0.0)
|
|
{
|
|
for (var i = 0; i < spaceCount; i++)
|
|
advancements[spaceIndexes[i]].width += extraWidth;
|
|
|
|
fragment._wordSpacing = extraWidth;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
[_layoutManager setLocation:CGPointMake(myX, _lineBase) forStartOfGlyphRange:lineRange];
|
|
[_layoutManager _setAdvancements:advancements forGlyphRange:lineRange];
|
|
|
|
// fix the _lineFragments when fontsizes differ
|
|
var l = _lineFragments.length;
|
|
|
|
for (var i = 0 ; i < l ; i++)
|
|
[_lineFragments[i] _adjustForHeight:_lineHeight];
|
|
|
|
if (!lineCount) // do not rescue on first line
|
|
return NO;
|
|
|
|
if (aContainer._inResizing)
|
|
return NO;
|
|
|
|
return ([_layoutManager _rescuingInvalidFragmentsWasPossibleForGlyphRange:lineRange]);
|
|
}
|
|
|
|
- (void)layoutGlyphsInLayoutManager:(CPLayoutManager)layoutManager
|
|
startingAtGlyphIndex:(unsigned)glyphIndex
|
|
maxNumberOfLineFragments:(unsigned)maxNumLines
|
|
nextGlyphIndex:(UIntegerReference)nextGlyph
|
|
{
|
|
var textContainers = [layoutManager textContainers],
|
|
textContainersCount = [textContainers count];
|
|
|
|
_layoutManager = layoutManager;
|
|
_textStorage = [_layoutManager textStorage];
|
|
_indexOfCurrentContainer = MAX(0, [textContainers
|
|
indexOfObject:[_layoutManager textContainerForGlyphAtIndex:glyphIndex effectiveRange:nil withoutAdditionalLayout:YES]
|
|
inRange:CPMakeRange(0, textContainersCount)]);
|
|
|
|
_currentTextContainer = textContainers[_indexOfCurrentContainer];
|
|
|
|
_attributesRange = CPMakeRange(0, 0);
|
|
_lineHeight = 0;
|
|
_lineBase = 0;
|
|
_lineWidth = 0;
|
|
|
|
// containerSizeWidth is the width inside the line fragment padding at both ends.
|
|
var containerSize = [_currentTextContainer containerSize],
|
|
lineFragmentPadding = [_currentTextContainer lineFragmentPadding],
|
|
containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding,
|
|
containerSizeHeight = containerSize.height,
|
|
lineRange = CPMakeRange(glyphIndex, 0),
|
|
wrapRange = CPMakeRange(0, 0),
|
|
wrapWidth = 0,
|
|
isNewline = NO,
|
|
isTabStop = NO,
|
|
isAttachment = NO,
|
|
isWordWrapped = NO,
|
|
numberOfGlyphs = [_textStorage length],
|
|
leading,
|
|
numLines = 0,
|
|
theString = [_textStorage string],
|
|
lineOrigin,
|
|
ascent,
|
|
descent,
|
|
advancements = [],
|
|
prevRangeWidth = 0,
|
|
measuringRange = CPMakeRange(glyphIndex, 0),
|
|
currentAnchor = 0,
|
|
currentFont,
|
|
currentFontLineHeight,
|
|
previousFont,
|
|
currentParagraphMinimumLineHeight,
|
|
currentParagraphMaximumLineHeight,
|
|
currentParagraphLineSpacing;
|
|
|
|
// Track physical line starts to prevent overwriting lineOrigin.x in tab segments
|
|
var isStartOfPhysicalLine = YES;
|
|
|
|
// Track paragraph indents and margins
|
|
var isFirstLineOfLayout = YES,
|
|
isFirstLineOfParagraph = YES,
|
|
rightMargin = containerSizeWidth;
|
|
|
|
if (glyphIndex > 0)
|
|
{
|
|
// Stored line fragment rectangles include the padding, which the layout below does not.
|
|
lineOrigin = CGPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin);
|
|
lineOrigin.x -= lineFragmentPadding;
|
|
}
|
|
else if ([_layoutManager extraLineFragmentTextContainer])
|
|
lineOrigin = CGPointMake(0, [_layoutManager extraLineFragmentUsedRect].origin.y);
|
|
else
|
|
lineOrigin = CGPointMake(0, 0);
|
|
|
|
[_layoutManager _removeInvalidLineFragments];
|
|
|
|
if (![_textStorage length])
|
|
return;
|
|
|
|
_lineFragments = [];
|
|
|
|
for (; numLines != maxNumLines && glyphIndex < numberOfGlyphs; glyphIndex++)
|
|
{
|
|
// check whether there is any change in the attributes from here on
|
|
if (!CPLocationInRange(glyphIndex, _attributesRange))
|
|
{
|
|
_currentAttributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:_attributesRange];
|
|
currentFont = [_currentAttributes objectForKey:CPFontAttributeName];
|
|
_currentParagraph = [_currentAttributes objectForKey:CPParagraphStyleAttributeName] || [CPParagraphStyle defaultParagraphStyle];
|
|
currentParagraphMinimumLineHeight = [_currentParagraph minimumLineHeight];
|
|
currentParagraphMaximumLineHeight = [_currentParagraph maximumLineHeight];
|
|
currentParagraphLineSpacing = [_currentParagraph lineSpacing];
|
|
|
|
// Recalculate right margin on paragraph style change
|
|
var tailIndent = [_currentParagraph tailIndent];
|
|
if (tailIndent > 0.0)
|
|
rightMargin = tailIndent;
|
|
else if (tailIndent < 0.0)
|
|
rightMargin = containerSizeWidth + tailIndent;
|
|
else
|
|
rightMargin = containerSizeWidth;
|
|
|
|
// If we are at the start of a physical line, we update lineOrigin.x
|
|
if (isStartOfPhysicalLine)
|
|
{
|
|
if (glyphIndex > 0)
|
|
{
|
|
var prevChar = theString.charCodeAt(glyphIndex - 1);
|
|
isFirstLineOfParagraph = (prevChar === 10 || prevChar === 13);
|
|
}
|
|
else
|
|
{
|
|
isFirstLineOfParagraph = YES;
|
|
}
|
|
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
|
isFirstLineOfLayout = NO;
|
|
}
|
|
|
|
// Handle the layout's very first line indentation
|
|
if (isFirstLineOfLayout)
|
|
{
|
|
if (glyphIndex > 0)
|
|
{
|
|
var prevChar = theString.charCodeAt(glyphIndex - 1);
|
|
isFirstLineOfParagraph = (prevChar === 10 || prevChar === 13);
|
|
}
|
|
else
|
|
{
|
|
isFirstLineOfParagraph = YES;
|
|
}
|
|
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
|
isFirstLineOfLayout = NO;
|
|
}
|
|
|
|
if (!currentFont)
|
|
currentFont = [_textStorage font] || [CPFont systemFontOfSize:12.0];
|
|
|
|
// Safely retrieve and validate CPBaselineOffsetAttributeName
|
|
var baselineOffset = [_currentAttributes objectForKey:CPBaselineOffsetAttributeName];
|
|
if (baselineOffset === nil || baselineOffset === undefined || typeof baselineOffset !== "number")
|
|
baselineOffset = 0.0;
|
|
|
|
// Safely retrieve and validate CPSuperscriptAttributeName
|
|
var superscript = [_currentAttributes objectForKey:CPSuperscriptAttributeName];
|
|
if (superscript === nil || superscript === undefined || typeof superscript !== "number")
|
|
superscript = 0;
|
|
|
|
if (superscript !== 0)
|
|
{
|
|
var size = [currentFont size],
|
|
scaledSize = size * 0.65,
|
|
fontName = [currentFont familyName],
|
|
isBold = [currentFont isBold],
|
|
isItalic = [currentFont isItalic];
|
|
|
|
currentFont = [CPFont _fontWithName:fontName size:scaledSize bold:isBold italic:isItalic];
|
|
|
|
if (baselineOffset === 0.0)
|
|
{
|
|
if (superscript > 0)
|
|
baselineOffset = size * 0.35;
|
|
else
|
|
baselineOffset = -size * 0.15;
|
|
}
|
|
}
|
|
|
|
var fontAscent = [currentFont ascender] || 0.0,
|
|
fontDescent = [currentFont descender] || 0.0;
|
|
|
|
ascent = fontAscent + baselineOffset;
|
|
descent = fontDescent + baselineOffset;
|
|
leading = (fontAscent - fontDescent) * 0.2; // FAKE leading
|
|
|
|
currentFontLineHeight = ascent - descent + leading;
|
|
|
|
if (previousFont !== currentFont)
|
|
{
|
|
measuringRange = CPMakeRange(glyphIndex, 0);
|
|
currentAnchor = prevRangeWidth;
|
|
previousFont = currentFont;
|
|
}
|
|
|
|
}
|
|
|
|
// Clean bounds logic to prevent NaN and layout calculation overhead
|
|
var currentAscent = (ascent === undefined || isNaN(ascent)) ? 0.0 : ascent,
|
|
currentLineHeight = (currentFontLineHeight === undefined || isNaN(currentFontLineHeight)) ? 12.0 : currentFontLineHeight;
|
|
|
|
if (currentLineHeight > _lineHeight)
|
|
_lineHeight = currentLineHeight;
|
|
|
|
if (currentAscent > _lineBase)
|
|
_lineBase = currentAscent;
|
|
|
|
lineRange.length++;
|
|
measuringRange.length++;
|
|
|
|
// We are processing characters, so we are no longer at the start of a physical line
|
|
isStartOfPhysicalLine = NO;
|
|
|
|
var currentCharCode = theString.charCodeAt(glyphIndex),
|
|
rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor;
|
|
|
|
switch (currentCharCode)
|
|
{
|
|
case CPAttachmentCharacter:
|
|
{
|
|
var attributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:nil],
|
|
view = [attributes objectForKey:_CPAttachmentView],
|
|
viewSize = view ? view._frame.size : CGSizeMake(0, 0);
|
|
|
|
rangeWidth = prevRangeWidth + viewSize.width; // undo sizing of dummy character
|
|
|
|
isAttachment = YES;
|
|
wrapRange = CPMakeRange(lineRange.location, lineRange.length - 1); // wrap before image
|
|
|
|
// prevent crash when image is larger than text container
|
|
if (viewSize.width > containerSizeWidth)
|
|
wrapRange.length++;
|
|
|
|
wrapWidth = rangeWidth;
|
|
wrapRange._height = _lineHeight;
|
|
wrapRange._base = _lineBase;
|
|
|
|
if (viewSize.height > _lineBase)
|
|
_lineBase = viewSize.height;
|
|
|
|
if (viewSize.height > _lineHeight)
|
|
_lineHeight = viewSize.height - descent + leading;
|
|
|
|
// An attachment wider than the container stays on the line it overflows, so a break
|
|
// there keeps the line's height and baseline with the attachment's size applied.
|
|
// Otherwise the attachment would be placed on the baseline of the text before it,
|
|
// and drawn above its line.
|
|
if (CPMaxRange(wrapRange) === CPMaxRange(lineRange))
|
|
{
|
|
wrapRange._height = _lineHeight;
|
|
wrapRange._base = _lineBase;
|
|
}
|
|
|
|
ascent = viewSize.height;
|
|
break;
|
|
}
|
|
case 9: // '\t'
|
|
{
|
|
// Measure against the actual text position before the tab stop
|
|
var nextTab = [self textTabForWidth:prevRangeWidth + lineOrigin.x writingDirection:0];
|
|
|
|
isTabStop = YES;
|
|
|
|
if (nextTab)
|
|
{
|
|
// Look-ahead to measure the width of the incoming text segment for alignment
|
|
var nextSegmentWidth = 0.0,
|
|
tempIndex = glyphIndex + 1,
|
|
segmentString = "";
|
|
|
|
while (tempIndex < numberOfGlyphs)
|
|
{
|
|
var nextCharCode = theString.charCodeAt(tempIndex);
|
|
if (nextCharCode === 9 || nextCharCode === 10 || nextCharCode === 13)
|
|
break;
|
|
segmentString += theString.charAt(tempIndex);
|
|
tempIndex++;
|
|
}
|
|
|
|
if (segmentString.length > 0)
|
|
nextSegmentWidth = [segmentString sizeWithFont:currentFont inWidth:NULL].width;
|
|
|
|
var tabLocation = [nextTab location],
|
|
tabAlignment = [nextTab alignment];
|
|
|
|
// Mathematically offset the tab character's right boundary
|
|
if ([nextTab tabStopType] === CPDecimalTabStopType)
|
|
{
|
|
// As in Cocoa, the first decimal separator of the user's locale starts at the stop.
|
|
// A segment without one ends at the stop, like a right tab.
|
|
var separatorIndex = segmentString.indexOf(_CPTypesetterDecimalSeparator()),
|
|
widthBeforeSeparator = separatorIndex === -1 ? nextSegmentWidth : [segmentString.substring(0, separatorIndex) sizeWithFont:currentFont inWidth:NULL].width;
|
|
|
|
rangeWidth = (tabLocation - widthBeforeSeparator) - lineOrigin.x;
|
|
}
|
|
else if (tabAlignment === CPCenterTextAlignment)
|
|
{
|
|
rangeWidth = (tabLocation - nextSegmentWidth / 2.0) - lineOrigin.x;
|
|
}
|
|
else if (tabAlignment === CPRightTextAlignment)
|
|
{
|
|
rangeWidth = (tabLocation - nextSegmentWidth) - lineOrigin.x;
|
|
}
|
|
else // Left align tab stop
|
|
{
|
|
rangeWidth = tabLocation - lineOrigin.x;
|
|
}
|
|
|
|
// Enforce a minimum safety spacer width to avoid character overlapping
|
|
var minRangeWidth = prevRangeWidth + 5.0;
|
|
if (rangeWidth < minRangeWidth)
|
|
rangeWidth = minRangeWidth;
|
|
}
|
|
else
|
|
{
|
|
rangeWidth = prevRangeWidth + 28.0; // standard fallback spacer
|
|
}
|
|
break;
|
|
}
|
|
case 32: // ' '
|
|
wrapRange = CPMakeRangeCopy(lineRange);
|
|
wrapWidth = rangeWidth;
|
|
wrapRange._height = _lineHeight;
|
|
wrapRange._base = _lineBase;
|
|
|
|
if (theString.charCodeAt(glyphIndex + 1) !== 32)
|
|
{
|
|
currentAnchor = rangeWidth;
|
|
measuringRange = CPMakeRange(glyphIndex + 1, 0);
|
|
}
|
|
|
|
break;
|
|
|
|
case 10:
|
|
case 13:
|
|
isNewline = YES;
|
|
}
|
|
|
|
advancements.push({width: rangeWidth - prevRangeWidth, height: ascent, descent: descent});
|
|
prevRangeWidth = _lineWidth = rangeWidth;
|
|
|
|
// Wrap lines against the tail indent (rightMargin) instead of container boundaries
|
|
if (lineOrigin.x + rangeWidth > rightMargin)
|
|
{
|
|
if (wrapWidth)
|
|
{
|
|
lineRange = wrapRange;
|
|
_lineWidth = wrapWidth;
|
|
_lineHeight = wrapRange._height;
|
|
_lineBase = wrapRange._base;
|
|
}
|
|
|
|
isNewline = YES;
|
|
isWordWrapped = YES;
|
|
glyphIndex = CPMaxRange(lineRange) - 1;
|
|
}
|
|
|
|
if (isNewline || isTabStop || isAttachment)
|
|
{
|
|
if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline wordWrapped:isWordWrapped rightMargin:rightMargin])
|
|
return;
|
|
|
|
if (isTabStop || isAttachment)
|
|
lineOrigin.x += rangeWidth;
|
|
|
|
if (isNewline)
|
|
{
|
|
if (currentParagraphMinimumLineHeight && currentParagraphMinimumLineHeight > _lineHeight)
|
|
_lineHeight = currentParagraphMinimumLineHeight;
|
|
|
|
if (currentParagraphMaximumLineHeight && currentParagraphMaximumLineHeight < _lineHeight)
|
|
_lineHeight = currentParagraphMaximumLineHeight;
|
|
|
|
lineOrigin.y += _lineHeight;
|
|
|
|
if (currentParagraphLineSpacing)
|
|
lineOrigin.y += currentParagraphLineSpacing;
|
|
|
|
if (lineOrigin.y > containerSizeHeight && _indexOfCurrentContainer < textContainersCount - 1)
|
|
{
|
|
_currentTextContainer = textContainers[++_indexOfCurrentContainer];
|
|
containerSize = [_currentTextContainer containerSize];
|
|
lineFragmentPadding = [_currentTextContainer lineFragmentPadding];
|
|
containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding;
|
|
containerSizeHeight = containerSize.height;
|
|
}
|
|
|
|
isFirstLineOfParagraph = !isWordWrapped;
|
|
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
|
|
|
|
numLines++;
|
|
isNewline = NO;
|
|
_lineFragments = [];
|
|
_lineHeight = 0;
|
|
_lineBase = 0;
|
|
isStartOfPhysicalLine = YES;
|
|
}
|
|
|
|
isTabStop = NO;
|
|
isAttachment = NO;
|
|
isWordWrapped = NO;
|
|
_lineWidth = 0;
|
|
advancements = [];
|
|
currentAnchor = 0;
|
|
prevRangeWidth = 0;
|
|
lineRange = CPMakeRange(glyphIndex + 1, 0);
|
|
measuringRange = CPMakeRange(glyphIndex + 1, 0);
|
|
wrapRange = CPMakeRange(0, 0);
|
|
wrapWidth = 0;
|
|
}
|
|
}
|
|
|
|
// Flush remaining characters
|
|
if (lineRange.length)
|
|
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO wordWrapped:NO rightMargin:rightMargin];
|
|
|
|
// The empty line after the text starts inside the padding, and at least 1 point in, so the insertion point stays visible.
|
|
var rect = CGRectMake(MAX(1.0, lineFragmentPadding), lineOrigin.y - descent, containerSizeWidth, currentFontLineHeight);
|
|
[_layoutManager setExtraLineFragmentRect:rect usedRect:rect textContainer:_currentTextContainer];
|
|
|
|
var fragment = [_layoutManager._lineFragments lastObject];
|
|
|
|
if (fragment)
|
|
fragment._isLast = YES;
|
|
}
|
|
|
|
@end
|