Files
cappuccino/AppKit/CPTextView/CPLayoutManager.j
T

1575 lines
46 KiB
Plaintext

/*
* CPLayoutManager.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
* 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 "CPText.j"
@import "CPTextContainer.j"
@import "CGContext.j"
@import "CPTypesetter.j"
@import "CPFont.j"
@global _MakeRangeFromAbs
@global document
@global CPBaselineOffsetAttributeName
@global CPSuperscriptAttributeName
@class CPTextContainer
@class CPTextView
function _isNewlineCharacter(chr)
{
return (chr === '\n' || chr === '\r');
}
function _RectEqualToRectHorizontally(lhsRect, rhsRect)
{
return (lhsRect.origin.x == rhsRect.origin.x &&
lhsRect.size.width == rhsRect.size.width &&
lhsRect.size.height == rhsRect.size.height);
}
_oncontextmenuhandler = function () { return false; };
/*!
@ingroup appkit
@class CPLayoutManager
*/
@implementation CPLayoutManager : CPObject
{
Class _lineFragmentFactory @accessors(setter=setLineFragmentFactory:);
CPMutableArray _textContainers @accessors(getter=textContainers);
CPTextStorage _textStorage @accessors(property=textStorage);
CPTypesetter _typesetter @accessors(property=typesetter);
CPMutableArray _lineFragments;
CPMutableArray _lineFragmentsForRescue;
id _extraLineFragment;
CPMutableArray _temporaryAttributes;
BOOL _isValidatingLayoutAndGlyphs;
CPRange _removeInvalidLineFragmentsRange;
}
#pragma mark -
#pragma mark Init methods
- (id)init
{
if (self = [super init])
{
[self _init];
}
return self;
}
- (void)_init
{
_isValidatingLayoutAndGlyphs = NO;
_lineFragmentFactory = [_CPLineFragment class];
_lineFragments = [[CPMutableArray alloc] init];
_textContainers = [[CPMutableArray alloc] init];
_textStorage = [[CPTextStorage alloc] init];
_typesetter = [CPTypesetter sharedSystemTypesetter];
[_textStorage addLayoutManager:self];
}
#pragma mark -
#pragma mark Text containes method
- (void)insertTextContainer:(CPTextContainer)aContainer atIndex:(int)index
{
[_textContainers insertObject:aContainer atIndex:index];
[aContainer setLayoutManager:self];
}
- (void)addTextContainer:(CPTextContainer)aContainer
{
[_textContainers addObject:aContainer];
[aContainer setLayoutManager:self];
}
- (void)removeTextContainerAtIndex:(int)index
{
var container = [_textContainers objectAtIndex:index];
[container setLayoutManager:nil];
[_textContainers removeObjectAtIndex:index];
}
// <!> fixme
- (int)numberOfGlyphs
{
return [_textStorage length];
}
- (int)numberOfCharacters
{
return [_textStorage length];
}
- (CPTextView)firstTextView
{
return [_textContainers[0] textView];
}
// from cocoa (?)
- (CPTextView)textViewForBeginningOfSelection
{
return [[_textContainers objectAtIndex:0] textView];
}
- (BOOL)layoutManagerOwnsFirstResponderInWindow:(CPWindow)aWindow
{
var firstResponder = [aWindow firstResponder],
c = [_textContainers count];
for (var i = 0; i < c; i++)
{
if ([_textContainers[i] textView] === firstResponder)
return YES;
}
return NO;
}
- (CGRect)boundingRectForGlyphRange:(CGRange)aRange inTextContainer:(CPTextContainer)container
{
if (![self numberOfGlyphs])
return CGRectMake(0, 0, 1, 12); // crude hack to give a cursor in an empty doc.
if (CPMaxRange(aRange) > [self numberOfGlyphs])
aRange = CPMakeRange([self numberOfGlyphs] - 1, 1);
var fragments = _objectsInRange(_lineFragments, aRange),
rect = nil,
c = [fragments count];
for (var i = 0; i < c; i++)
{
var fragment = fragments[i];
if (fragment._textContainer === container)
{
var frames = [fragment glyphFrames],
l = frames ? frames.length : 0;
for (var j = 0; j < l; j++)
{
if (CPLocationInRange(fragment._range.location + j, aRange))
{
if (!rect)
rect = CGRectCreateCopy(frames[j]);
else
rect = CGRectUnion(rect, frames[j]);
}
}
}
}
return rect ? rect : CGRectMakeZero();
}
- (CPRange)glyphRangeForTextContainer:(CPTextContainer)aTextContainer
{
var range = nil,
c = [_lineFragments count];
for (var i = 0; i < c; i++)
{
var fragment = _lineFragments[i];
if (fragment._textContainer === aTextContainer)
{
if (!range)
range = CPMakeRangeCopy(fragment._range);
else
range = CPUnionRange(range, fragment._range);
}
}
return range ? range : CPMakeRange(CPNotFound, 0);
}
- (void)_removeInvalidLineFragments
{
_lineFragmentsForRescue = [_lineFragments copy];
[_lineFragmentsForRescue makeObjectsPerformSelector:@selector(_deinvalidate)];
if (_removeInvalidLineFragmentsRange && _removeInvalidLineFragmentsRange.length && _lineFragments.length)
{
// [[_lineFragments subarrayWithRange:_removeInvalidLineFragmentsRange] makeObjectsPerformSelector:@selector(invalidate)];
[_lineFragments removeObjectsInRange:_removeInvalidLineFragmentsRange];
[[_lineFragmentsForRescue subarrayWithRange:_removeInvalidLineFragmentsRange] makeObjectsPerformSelector:@selector(invalidate)];
}
}
- (void)_cleanUpDOM
{
var l = _lineFragmentsForRescue? _lineFragmentsForRescue.length : 0;
for (var i = 0; i < l; i++)
{
if (_lineFragmentsForRescue[i]._isInvalid)
[_lineFragmentsForRescue[i] _removeFromDOM];
}
}
- (void)_validateLayoutAndGlyphs
{
if (_isValidatingLayoutAndGlyphs)
return;
_isValidatingLayoutAndGlyphs = YES;
var startIndex = CPNotFound,
removeRange = CPMakeRange(0, 0),
l = _lineFragments.length;
if (l)
{
for (var i = 0; i < l; i++)
{
if (_lineFragments[i]._isInvalid)
{
while (i > 0 && !_lineFragments[i - 1]._isLast)
i--;
startIndex = _lineFragments[i]._range.location;
removeRange.location = i;
removeRange.length = l - i;
break;
}
}
// start one line above current line to make sure that a word can jump up
if (startIndex == CPNotFound && CPMaxRange (_lineFragments[l - 1]._range) < [_textStorage length])
startIndex = CPMaxRange(_lineFragments[l - 1]._range);
}
else
{
startIndex = 0;
}
/* nothing to validate and layout */
if (startIndex == CPNotFound)
{
_isValidatingLayoutAndGlyphs = NO;
return;
}
if (removeRange.length)
_removeInvalidLineFragmentsRange = CPMakeRangeCopy(removeRange);
else
_removeInvalidLineFragmentsRange = nil;
// We erased all lines
if (!startIndex)
[self setExtraLineFragmentRect:CGRectMake(0, 0) usedRect:CGRectMake(0, 0) textContainer:nil];
[_typesetter layoutGlyphsInLayoutManager:self startingAtGlyphIndex:startIndex maxNumberOfLineFragments:-1 nextGlyphIndex:nil];
#if PLATFORM(DOM)
[self _cleanUpDOM];
#endif
_isValidatingLayoutAndGlyphs = NO;
}
- (BOOL)_rescuingInvalidFragmentsWasPossibleForGlyphRange:(CPRange)aRange
{
// 1. EARLY EXIT: If there are no fragments to rescue (e.g. setting new text), do nothing.
if (!_lineFragmentsForRescue || _lineFragmentsForRescue.length === 0)
return NO;
var l = _lineFragments.length,
location = aRange.location,
found = NO,
targetLine = l - 1; // Start from the END of the array
// 2. REVERSE SEARCH: The fragment we want is almost always at the end.
for (; targetLine >= 0; targetLine--)
{
if (CPLocationInRange(location, _lineFragments[targetLine]._range))
{
found = YES;
break;
}
}
if (!found)
return NO;
if (!_lineFragmentsForRescue[targetLine])
return NO;
var startLineForDOMRemoval = targetLine,
isIdentical = YES,
newLineFragment= _lineFragments[targetLine],
oldLineFragment = _lineFragmentsForRescue[targetLine],
oldLength = CPMaxRange([_lineFragmentsForRescue lastObject]._range),
newLength = [[_textStorage string].length],
removalSkip = 1;
if (![oldLineFragment isVisuallyIdenticalToFragment:newLineFragment])
{
isIdentical = NO;
// deleting newline in its own line-> move up instead of re-layouting
if (newLength < oldLength && oldLineFragment._range.length == 1 && newLineFragment._range.length > 1 && newLineFragment._range.location === oldLineFragment._range.location && oldLineFragment._isLast)
{
isIdentical = YES;
targetLine--;
removalSkip++;
}
// newline entered in its own line-> move down instead of re-layouting
if (newLength > oldLength && newLineFragment._range.length == 1 && oldLineFragment._range.length > 1 && newLineFragment._range.location === oldLineFragment._range.location && newLineFragment._isLast)
{
isIdentical = YES;
startLineForDOMRemoval--;
}
}
// patch the linefragments instead of re-layoutung
if (isIdentical)
{
var rangeOffset = CPMaxRange(_lineFragments[targetLine]._range) - CPMaxRange(_lineFragmentsForRescue[startLineForDOMRemoval]._range);
if (ABS(rangeOffset) !== ABS(newLength - oldLength))
return NO;
var verticalOffset = CGRectGetMaxY(_lineFragments[targetLine]._fragmentRect) - CGRectGetMaxY(_lineFragmentsForRescue[startLineForDOMRemoval]._fragmentRect),
l = _lineFragmentsForRescue.length,
newTargetLine = startLineForDOMRemoval + removalSkip;
for (; newTargetLine < l; newTargetLine++)
{
_lineFragmentsForRescue[newTargetLine]._isInvalid = NO; // protect them from final removal
[_lineFragmentsForRescue[newTargetLine] _relocateVerticallyByY:verticalOffset rangeOffset:rangeOffset];
_lineFragments.push(_lineFragmentsForRescue[newTargetLine]);
}
}
return isIdentical;
}
- (void)invalidateDisplayForGlyphRange:(CPRange)range
{
var lineFragments = _objectsInRange(_lineFragments, range);
for (var i = 0; i < lineFragments.length; i++)
[[lineFragments[i]._textContainer textView] setNeedsDisplayInRect:lineFragments[i]._fragmentRect];
}
- (void)invalidateLayoutForCharacterRange:(CPRange)aRange isSoft:(BOOL)flag actualCharacterRange:(CPRangePointer)actualCharRange
{
var firstFragmentIndex = _lineFragments.length ? [_lineFragments _indexOfObject: aRange.location sortedByFunction:_sortRange context:nil] : CPNotFound;
if (firstFragmentIndex == CPNotFound)
{
if (_lineFragments.length)
{
firstFragmentIndex = _lineFragments.length - 1;
}
else
{
if (actualCharRange)
{
actualCharRange.length = aRange.length;
actualCharRange.location = 0;
}
return;
}
}
else
{
firstFragmentIndex = firstFragmentIndex + (firstFragmentIndex ? -1 : 0);
}
var fragment = _lineFragments[firstFragmentIndex],
range = CPMakeRangeCopy(fragment._range);
fragment._isInvalid = YES;
/* invalidated all fragments that follow */
for (var i = firstFragmentIndex + 1; i < _lineFragments.length; i++)
{
_lineFragments[i]._isInvalid = YES;
range = CPUnionRange(range, _lineFragments[i]._range);
}
if (CPMaxRange(range) < CPMaxRange(aRange))
range = CPUnionRange(range, aRange);
if (actualCharRange)
{
actualCharRange.length = range.length;
actualCharRange.location = range.location;
}
}
- (void)textStorage:(CPTextStorage)textStorage edited:(unsigned)mask range:(CPRange)charRange changeInLength:(int)delta invalidatedRange:(CPRange)invalidatedRange
{
var actualRange = CPMakeRange(CPNotFound,0);
[self invalidateLayoutForCharacterRange:invalidatedRange isSoft:NO actualCharacterRange:actualRange];
[self invalidateDisplayForGlyphRange:actualRange];
[self _validateLayoutAndGlyphs];
[[self firstTextView] sizeToFit];
}
- (CPRange)glyphRangeForBoundingRect:(CGRect)aRect inTextContainer:(CPTextContainer)container
{
var c = [_lineFragments count],
range;
for (var i = 0; i < c; i++)
{
var fragment = _lineFragments[i];
if (fragment._textContainer === container)
{
if (CGRectContainsRect(aRect, fragment._fragmentRect))
{
if (!range)
range = CPMakeRangeCopy(fragment._range);
else
range = CPUnionRange(range, fragment._range);
}
else
{
var glyphRange = CPMakeRange(CPNotFound, 0),
frames = [fragment glyphFrames];
for (var j = 0; j < frames.length; j++)
{
if (CGRectIntersectsRect(aRect, frames[j]))
{
if (glyphRange.location == CPNotFound)
glyphRange.location = fragment._range.location + j;
else
glyphRange.length++;
}
}
if (glyphRange.location != CPNotFound)
{
if (!range)
range = CPMakeRangeCopy(glyphRange);
else
range = CPUnionRange(range, glyphRange);
}
}
}
}
return range ? range : CPMakeRange(0,0);
}
- (void)drawBackgroundForGlyphRange:(CPRange)aRange atPoint:(CGPoint)aPoint
{
}
- (void)drawGlyphsForGlyphRange:(CPRange)aRange atPoint:(CGPoint)aPoint
{
var lineFragments = _objectsInRange(_lineFragments, aRange);
if (!lineFragments.length)
return;
var paintedRange = CPMakeRangeCopy(aRange),
l = lineFragments.length,
lineFragmentIndex,
ctx;
for (lineFragmentIndex = 0; lineFragmentIndex < l; lineFragmentIndex++)
{
var currentFragment = lineFragments[lineFragmentIndex];
[currentFragment drawInContext:ctx atPoint:aPoint forRange:paintedRange];
}
}
- (unsigned)glyphIndexForPoint:(CGPoint)point inTextContainer:(CPTextContainer)container fractionOfDistanceThroughGlyph:(FloatArray)partialFraction
{
var c = [_lineFragments count];
for (var i = 0; i < c; i++)
{
var fragment = _lineFragments[i];
if (fragment._textContainer === container)
{
var frames = [fragment glyphFrames],
len = fragment._range.length;
if (frames)
{
var maxLen = MIN(len, frames.length);
for (var j = 0; j < maxLen; j++)
{
var frame = frames[j];
if (frame && CGRectContainsPoint(frame, point))
{
if (partialFraction)
partialFraction[0] = (point.x - frame.origin.x) / frame.size.width;
return fragment._range.location + j;
}
}
}
}
}
if ([[_textStorage string] length])
{
for (var i = 0; i < c; i++)
{
var fragment = _lineFragments[i];
if (fragment._textContainer === container)
{
if (fragment._range.length > 0 && point.y > fragment._fragmentRect.origin.y &&
point.y <= fragment._fragmentRect.origin.y + fragment._fragmentRect.size.height)
{
if (i < c - 1 && _lineFragments[i + 1]._fragmentRect.origin.y === fragment._fragmentRect.origin.y)
continue;
var nlLoc = CPMaxRange(fragment._range),
frames = [fragment glyphFrames];
if (frames && frames.length > 0)
{
var lastFrame = frames[frames.length - 1],
firstFrame = frames[0];
if (lastFrame && firstFrame)
{
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:nlLoc > 0 ? nlLoc - 1 : 0]))
nlLoc--;
if (point.x > CGRectGetMaxX(lastFrame))
return nlLoc;
else if (point.x <= CGRectGetMinX(firstFrame))
return fragment._range.location;
else
return nlLoc;
}
}
}
}
}
}
return point.y > 0 ? [[_textStorage string] length] : 0;
}
- (unsigned)glyphIndexForPoint:(CGPoint)point inTextContainer:(CPTextContainer)container
{
return [self glyphIndexForPoint:point inTextContainer:container fractionOfDistanceThroughGlyph:nil];
}
- (void)_setAttributes:(CPDictionary)attributes toTemporaryAttributes:(_CPTemporaryAttributes)tempAttributes
{
tempAttributes._attributes = attributes;
}
- (void)_addAttributes:(CPDictionary)attributes toTemporaryAttributes:(_CPTemporaryAttributes)tempAttributes
{
[tempAttributes._attributes addEntriesFromDictionary:attributes];
}
- (void)_handleTemporaryAttributes:(CPDictionary)attributes forCharacterRange:(CPRange)charRange withSelector:(SEL)attributesOperation
{
// FIXME
}
- (void)setTemporaryAttributes:(CPDictionary)attributes forCharacterRange:(CPRange)charRange
{
[self _handleTemporaryAttributes:attributes forCharacterRange:charRange withSelector:@selector(_setAttributes:toTemporaryAttributes:)];
}
- (void)addTemporaryAttributes:(CPDictionary)attributes forCharacterRange:(CPRange)charRange
{
[self _handleTemporaryAttributes:attributes forCharacterRange:charRange withSelector:@selector(_addAttributes:toTemporaryAttributes:)];
}
- (void)removeTemporaryAttribute:(CPString)attributeName forCharacterRange:(CPRange)charRange
{
// FIXME
}
- (CPDictionary)temporaryAttributesAtCharacterIndex:(unsigned)index effectiveRange:(CPRangePointer)effectiveRange
{
// FIXME
}
- (void)textContainerChangedTextView:(CPTextContainer)aContainer
{
// FIXME
}
- (void)_appendNewLineFragmentInTextContainer:(CPTextContainer)aTextContainer forGlyphRange:(CPRange)glyphRange
{
_lineFragments.push([[_lineFragmentFactory alloc] initWithRange:glyphRange textContainer:aTextContainer textStorage:_textStorage]);
}
- (void)setTextContainer:(CPTextContainer)aTextContainer forGlyphRange:(CPRange)glyphRange
{
var fragments = _objectsInRange(_lineFragments, glyphRange),
l = fragments.length;
for (var i = 0; i < l; i++)
fragments[i]._textContainer = aTextContainer;
}
- (id)_lineFragmentForLocation:(unsigned) aLoc
{
var fragments = _objectsInRange(_lineFragments, CPMakeRange(aLoc, 0)),
l = fragments.length;
if (l > 0)
return fragments[0];
return nil;
}
- (id)_firstLineFragmentForLineFromLocation:(unsigned)location
{
var l = _lineFragments.length;
for (var i = 0; i < l; i++)
{
if (CPLocationInRange(location, _lineFragments[i]._range))
{
var j = i;
while (j > 0 && !_lineFragments[j - 1]._isLast)
j--;
return _lineFragments[j];
}
}
return nil;
}
- (id)_lastLineFragmentForLineFromLocation:(unsigned)location
{
var l = _lineFragments.length;
if (location >= CPMaxRange(_lineFragments[l - 1]._range))
return _lineFragments[l - 1];
for (var i = 0; i < l; i++)
{
if (CPLocationInRange(location, _lineFragments[i]._range))
{
var j = i;
while (j < l && !_lineFragments[j]._isLast)
j++;
return _lineFragments[j];
}
}
return nil;
}
- (double)_characterOffsetAtLocation:(unsigned)location
{
var lineFragment = _objectWithLocationInRange(_lineFragments, location);
if (!lineFragment)
return 0.0;
var index = location - lineFragment._range.location;
if (index < 0 || !lineFragment._glyphsOffsets || index >= lineFragment._glyphsOffsets.length)
return 0.0;
var offset = lineFragment._glyphsOffsets[index];
return (offset === undefined) ? 0.0 : offset;
}
- (double)_descentAtLocation:(unsigned)location
{
var lineFragment = _objectWithLocationInRange(_lineFragments, location);
if (!lineFragment)
return 0.0;
var index = location - lineFragment._range.location;
if (index < 0 || !lineFragment._glyphsFrames || index >= lineFragment._glyphsFrames.length)
return 0.0;
var frame = lineFragment._glyphsFrames[index];
return (frame && frame._descent !== undefined) ? frame._descent : 0.0;
}
- (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(CPRange)glyphRange usedRect:(CGRect)usedRect
{
var lineFragment = _objectWithLocationInRange(_lineFragments, glyphRange.location);
if (lineFragment)
{
lineFragment._fragmentRect = CGRectCreateCopy(fragmentRect);
lineFragment._usedRect = CGRectCreateCopy(usedRect);
}
}
- (void)_setAdvancements:(CPArray)someAdvancements forGlyphRange:(CPRange)glyphRange
{
var lineFragment = _objectWithLocationInRange(_lineFragments, glyphRange.location);
if (lineFragment)
[lineFragment setAdvancements:someAdvancements];
}
- (void)setLocation:(CGPoint)aPoint forStartOfGlyphRange:(CPRange)glyphRange
{
var lineFragment = _objectWithLocationInRange(_lineFragments, glyphRange.location);
if (lineFragment)
lineFragment._location = CGPointCreateCopy(aPoint);
}
- (CGRect)extraLineFragmentRect
{
if (_extraLineFragment)
return CGRectCreateCopy(_extraLineFragment._fragmentRect);
return CGRectMakeZero();
}
- (CPTextContainer)extraLineFragmentTextContainer
{
if (_extraLineFragment)
return _extraLineFragment._textContainer;
return nil;
}
- (CGRect)extraLineFragmentUsedRect
{
if (_extraLineFragment)
return CGRectCreateCopy(_extraLineFragment._usedRect);
return CGRectMakeZero();
}
- (void)setExtraLineFragmentRect:(CGRect)rect usedRect:(CGRect)usedRect textContainer:(CPTextContainer)textContainer
{
if (textContainer)
{
_extraLineFragment = {};
_extraLineFragment._fragmentRect = CGRectCreateCopy(rect);
_extraLineFragment._usedRect = CGRectCreateCopy(usedRect);
_extraLineFragment._textContainer = textContainer;
}
else
{
_extraLineFragment = nil;
}
}
- (CGRect)usedRectForTextContainer:(CPTextContainer)textContainer
{
var rect,
l = _lineFragments.length;
for (var i = 0; i < l; i++)
{
if (_lineFragments[i]._textContainer === textContainer)
{
if (rect)
rect = CGRectUnion(rect, _lineFragments[i]._usedRect);
else
rect = CGRectCreateCopy(_lineFragments[i]._usedRect);
}
}
return rect ? rect : CGRectMakeZero();
}
- (CGRect)lineFragmentRectForGlyphAtIndex:(unsigned)glyphIndex effectiveRange:(CPRangePointer)effectiveGlyphRange
{
var lineFragment = _objectWithLocationInRange(_lineFragments, glyphIndex);
if (!lineFragment)
return CGRectMakeZero();
if (effectiveGlyphRange)
{
effectiveGlyphRange.location = lineFragment._range.location;
effectiveGlyphRange.length = lineFragment._range.length;
}
return CGRectCreateCopy(lineFragment._fragmentRect);
}
- (CGRect)lineFragmentUsedRectForGlyphAtIndex:(unsigned)glyphIndex effectiveRange:(CPRangePointer)effectiveGlyphRange
{
var lineFragment = _objectWithLocationInRange(_lineFragments, glyphIndex);
if (!lineFragment)
return CGRectMakeZero();
if (effectiveGlyphRange)
{
effectiveGlyphRange.location = lineFragment._range.location;
effectiveGlyphRange.length = lineFragment._range.length;
}
return CGRectCreateCopy(lineFragment._usedRect);
}
- (CGPoint)locationForGlyphAtIndex:(unsigned)index
{
if (_lineFragments.length > 0 && index >= [self numberOfGlyphs] - 1)
{
var lineFragment = _lineFragments[_lineFragments.length - 1],
glyphFrames = [lineFragment glyphFrames];
if (glyphFrames && glyphFrames.length > 0)
{
var frame = glyphFrames[glyphFrames.length - 1];
if (frame)
return CGPointCreateCopy(frame.origin);
}
}
var lineFragment = _objectWithLocationInRange(_lineFragments, index);
if (lineFragment)
{
if (index == lineFragment._range.location)
return CGPointCreateCopy(lineFragment._location);
var glyphFrames = [lineFragment glyphFrames];
var relativeIndex = index - lineFragment._range.location;
if (glyphFrames && relativeIndex >= 0 && relativeIndex < glyphFrames.length)
{
var frame = glyphFrames[relativeIndex];
if (frame)
return CGPointCreateCopy(frame.origin);
}
return CGPointCreateCopy(lineFragment._location);
}
return CGPointMakeZero();
}
- (CPTextContainer)textContainerForGlyphAtIndex:(unsigned)index effectiveRange:(CPRangePointer)effectiveGlyphRange withoutAdditionalLayout:(BOOL)flag
{
var lineFragment = _objectWithLocationInRange(_lineFragments, index);
if (lineFragment)
{
if (effectiveGlyphRange)
{
effectiveGlyphRange.location = lineFragment._range.location;
effectiveGlyphRange.length = lineFragment._range.length;
}
return lineFragment._textContainer;
}
return [_textContainers lastObject];
}
- (CPTextContainer)textContainerForGlyphAtIndex:(unsigned)index effectiveRange:(CPRangePointer)effectiveGlyphRange
{
return [self textContainerForGlyphAtIndex:index effectiveRange:effectiveGlyphRange withoutAdditionalLayout:NO];
}
- (CPRange)characterRangeForGlyphRange:(CPRange)aRange actualGlyphRange:(CPRangePointer)actualRange
{
return _MakeRangeFromAbs([self characterIndexForGlyphAtIndex:aRange.location],
[self characterIndexForGlyphAtIndex:CPMaxRange(aRange)]);
}
- (unsigned)characterIndexForGlyphAtIndex:(unsigned)index
{
/* FIXME: stub */
return index;
}
- (CPArray)rectArrayForCharacterRange:(CPRange)charRange
withinSelectedCharacterRange:(CPRange)selectedCharRange
inTextContainer:(CPTextContainer)container
rectCount:(CGRectPointer)rectCount
{
var rectArray = [],
lineFragments = _objectsInRange(_lineFragments, selectedCharRange);
if (!lineFragments.length)
return rectArray;
var containerSize = [container containerSize];
for (var i = 0; i < lineFragments.length; i++)
{
var fragment = lineFragments[i];
if (fragment._textContainer === container)
{
var frames = [fragment glyphFrames],
rect = nil,
len = fragment._range.length;
if (frames)
{
for (var j = 0; j < len; j++)
{
if (j < frames.length && CPLocationInRange(fragment._range.location + j, selectedCharRange))
{
var frame = frames[j];
if (frame)
{
var correctedRect = CGRectCreateCopy(frame);
correctedRect.size.height -= frame._descent;
correctedRect.origin.y -= frame._descent;
if (!rect)
rect = CGRectCreateCopy(correctedRect);
else
rect = CGRectUnion(rect, correctedRect);
if (_isNewlineCharacter([[_textStorage string] characterAtIndex:MAX(0, CPMaxRange(selectedCharRange) - 1)]))
rect.size.width = containerSize.width - rect.origin.x;
}
}
}
}
if (rect)
rectArray.push(rect);
}
}
var len = rectArray.length;
for (var i = 0; i < len - 1; i++)
{
if (FLOOR(CGRectGetMaxY(rectArray[i])) == FLOOR(CGRectGetMaxY(rectArray[i + 1])))
continue;
rectArray[i].size.width = containerSize.width - rectArray[i].origin.x;
}
return rectArray;
}
@end
var CPLayoutManagerTextStorageKey = @"CPLayoutManagerTextStorageKey";
@implementation CPLayoutManager (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super init];
if (self)
{
[self _init];
_textStorage = [aCoder decodeObjectForKey:CPLayoutManagerTextStorageKey];
[_textStorage addLayoutManager:self];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeObject:_textStorage forKey:CPLayoutManagerTextStorageKey];
}
@end
@implementation CPArray (SortedSearching)
- (unsigned)_indexOfObject:(id)anObject sortedByFunction:(Function)aFunction context:(id)aContext
{
var length= self.length;
if (!aFunction)
return CPNotFound;
if (length === 0)
return -1;
var mid,
c,
first = 0,
last = length - 1;
while (first <= last)
{
mid = FLOOR((first + last) / 2);
c = aFunction(anObject, self[mid], aContext);
if (c > 0)
{
first = mid + 1;
}
else if (c < 0)
{
last = mid - 1;
}
else
{
while (mid < length - 1 && aFunction(anObject, self[mid + 1], aContext) == CPOrderedSame)
mid++;
return mid;
}
}
var result = -first - 1;
return result >= 0 ? result : CPNotFound;
}
@end
var _sortRange = function(location, anObject)
{
if (CPLocationInRange(location, anObject._range))
return CPOrderedSame;
else if (CPMaxRange(anObject._range) <= location)
return CPOrderedDescending;
else
return CPOrderedAscending;
}
var _objectWithLocationInRange = function(aList, aLocation)
{
var index = [aList _indexOfObject:aLocation sortedByFunction:_sortRange context:nil];
if (index != CPNotFound)
return aList[index];
return nil;
}
var _objectsInRange = function(aList, aRange)
{
var firstIndex = [aList _indexOfObject:aRange.location sortedByFunction:_sortRange context:nil],
lastIndex = [aList _indexOfObject:CPMaxRange(aRange) sortedByFunction:_sortRange context:nil];
if (firstIndex === CPNotFound)
firstIndex = 0;
if (lastIndex === CPNotFound)
lastIndex = aList.length - 1;
return aList.slice(firstIndex, lastIndex + 1);
}
@implementation _CPLineFragment : CPObject
{
CPArray _glyphsFrames @accessors(getter=glyphFrames);
CPArray _glyphsOffsets;
BOOL _isInvalid;
BOOL _isLast;
CGRect _fragmentRect;
CGRect _usedRect;
CGPoint _location;
CPRange _range;
CPTextContainer _textContainer;
CPMutableArray _runs;
}
#pragma mark -
#pragma mark Init methods
- (id)createDOMElementWithText:(CPString)aString andFont:(CPFont)aFont andColor:(CPColor)aColor
{
return [self createDOMElementWithText:aString andFont:aFont andColor:aColor andBackgroundColor:nil andUnderline:nil ];
}
- (id)createDOMElementWithText:(CPString)aString andFont:(CPFont)aFont andColor:(CPColor)fgColor andBackgroundColor:(CPColor)bgColor andUnderline:(CPUnderlineStyle)aUnderline
{
if (!aString || aString.length === 0)
return nil;
#if PLATFORM(DOM)
var style,
span = document.createElement("span");
span.oncontextmenu = span.onmousedown = span.onselectstart = _oncontextmenuhandler;
style = span.style;
style.position = "absolute";
style.visibility = "visible";
style.padding = "0px";
style.margin = "0px";
style.whiteSpace = "pre";
style.backgroundColor = "transparent";
style.font = [aFont cssString];
if (aUnderline)
{
style.textDecoration = "underline";
switch (aUnderline)
{
case CPUnderlineStyleSingle:
style.textDecorationStyle = "solid";
break;
case CPUnderlineStyleDouble:
style.textDecorationStyle = "double";
break;
case CPUnderlineStylePatternDot:
style.textDecorationStyle = "dotted";
break;
case CPUnderlineStylePatternDash:
style.textDecorationStyle = "dashed";
break;
}
}
if (fgColor)
style.color = [fgColor cssString];
if (bgColor)
style.backgroundColor = [bgColor cssString];
if (CPFeatureIsCompatible(CPJavaScriptInnerTextFeature))
span.innerText = aString;
else if (CPFeatureIsCompatible(CPJavaScriptTextContentFeature))
span.textContent = aString;
return span;
#else
return nil;
#endif
}
- (id)initWithRange:(CPRange)aRange textContainer:(CPTextContainer)aContainer textStorage:(CPTextStorage)textStorage
{
if (self = [super init])
{
var effectiveRange = CPMakeRange(0,0),
location;
_fragmentRect = CGRectMakeZero();
_usedRect = CGRectMakeZero();
_location = CGPointMakeZero();
_range = CPMakeRangeCopy(aRange);
_textContainer = aContainer;
_isInvalid = NO;
_runs = [];
_glyphsFrames = [];
_glyphsOffsets = [];
for (location = aRange.location; location < CPMaxRange(aRange); location = CPMaxRange(effectiveRange))
{
var attributes = [textStorage attributesAtIndex:location effectiveRange:effectiveRange];
effectiveRange = attributes ? CPIntersectionRange(aRange, effectiveRange) : aRange;
var string = [textStorage._string substringWithRange:effectiveRange],
underline = [attributes objectForKey:CPUnderlineStyleAttributeName] || CPUnderlineStyleNone,
paragraphStyle = [attributes objectForKey:CPParagraphStyleAttributeName] || [CPParagraphStyle defaultParagraphStyle];
// this is an attachment -> create a run for it
if (string === _CPAttachmentCharacterAsString)
{
if (![attributes objectForKey:_CPAttachmentInvisible])
{
var view = [attributes objectForKey:_CPAttachmentView];
var run = {_range:CPMakeRangeCopy(effectiveRange), color:nil, font:nil, elem:nil, string:nil, view:view, paragraphStyle:paragraphStyle, underline:underline, baselineOffset:0.0};
_runs.push(run);
}
}
else
{
var color = [attributes objectForKey:CPForegroundColorAttributeName],
bgcolor = [attributes objectForKey:CPBackgroundColorAttributeName],
font = [attributes objectForKey:CPFontAttributeName] || [textStorage font] || [CPFont systemFontOfSize:12.0];
var baselineOffset = [attributes objectForKey:CPBaselineOffsetAttributeName],
superscript = [attributes objectForKey:CPSuperscriptAttributeName];
if (baselineOffset === nil || baselineOffset === undefined || typeof baselineOffset !== "number")
baselineOffset = 0.0;
if (superscript === nil || superscript === undefined || typeof superscript !== "number")
superscript = 0;
if (superscript !== 0)
{
var size = [font size],
scaledSize = size * 0.65,
fontName = [font familyName],
isBold = [font isBold],
isItalic = [font isItalic];
font = [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 currentLoc = effectiveRange.location,
strLen = string.length,
startIdx = 0;
for (var i = 0; i < strLen; i++)
{
if (string.charCodeAt(i) === 9) // Tabulator-Zeichen '\t'
{
if (i > startIdx)
{
var subString = string.substring(startIdx, i),
subRange = CPMakeRange(currentLoc + startIdx, i - startIdx),
run = {
_range: subRange,
color: color,
font: font,
elem: nil,
string: subString,
bgcolor: bgcolor,
paragraphStyle: paragraphStyle,
underline: underline,
baselineOffset: baselineOffset
};
_runs.push(run);
}
var tabRange = CPMakeRange(currentLoc + i, 1),
tabRun = {
_range: tabRange,
color: nil,
font: nil,
elem: nil,
string: nil,
bgcolor: nil,
paragraphStyle: paragraphStyle,
underline: underline,
baselineOffset: 0.0
};
_runs.push(tabRun);
startIdx = i + 1;
}
}
if (startIdx < strLen)
{
var subString = string.substring(startIdx, strLen),
subRange = CPMakeRange(currentLoc + startIdx, strLen - startIdx),
run = {
_range: subRange,
color: color,
font: font,
elem: nil,
string: subString,
bgcolor: bgcolor,
paragraphStyle: paragraphStyle,
underline: underline,
baselineOffset: baselineOffset
};
_runs.push(run);
}
}
if (!CPMaxRange(effectiveRange))
break;
}
}
return self;
}
- (void)setAdvancements:(CPArray)someAdvancements
{
var count = someAdvancements.length,
origin = CGPointMake(_fragmentRect.origin.x + _location.x, _fragmentRect.origin.y),
height = _usedRect.size.height;
_glyphsFrames = new Array(count);
_glyphsOffsets = new Array(count);
for (var i = 0; i < count; i++)
{
_glyphsFrames[i] = CGRectMake(origin.x, origin.y, someAdvancements[i].width, height);
_glyphsFrames[i]._descent = someAdvancements[i].descent;
_glyphsOffsets[i] = height - someAdvancements[i].height;
origin.x += someAdvancements[i].width;
}
}
- (void)_adjustForHeight:(double)height
{
var count = _glyphsFrames.length;
for (var i = 0; i < count; i++)
_glyphsFrames[i].origin.y += (height - _fragmentRect.size.height);
_fragmentRect.size.height = height;
}
- (CPString)description
{
return [super description] +
"\n\t_fragmentRect="+CPStringFromRect(_fragmentRect) +
"\n\t_usedRect="+CPStringFromRect(_usedRect) +
"\n\t_location="+CPStringFromPoint(_location) +
"\n\t_range="+CPStringFromRange(_range);
}
- (void)drawUnderlineForGlyphRange:(CPRange)glyphRange
underlineType:(int)underlineVal
baselineOffset:(float)baselineOffset
containerOrigin:(CGPoint)containerOrigin
{
// <!> FIXME
}
- (void)invalidate
{
_isInvalid = YES;
}
- (void)_deinvalidate
{
_isInvalid = NO;
}
- (void)_removeFromDOM
{
var l = _runs.length;
for (var i = 0; i < l; i++)
{
if (_runs[i].view && _runs[i].DOMactive)
[_runs[i].view removeFromSuperview];
if (_runs[i].elem && _runs[i].DOMactive)
_textContainer._textView._DOMElement.removeChild(_runs[i].elem);
_runs[i].elem = nil;
_runs[i].DOMactive = NO;
}
}
- (void)drawInContext:(CGContext)context atPoint:(CGPoint)aPoint forRange:(CPRange)aRange
{
var runs = _objectsInRange(_runs, aRange),
c = runs.length,
orig = CGPointMake(_fragmentRect.origin.x, _fragmentRect.origin.y);
if (_runs.length === 0)
return;
for (var i = 0; i < c; i++)
{
var run = runs[i];
if (!run.elem && CPRectIntersectsRect([_textContainer._textView exposedRect], _fragmentRect))
run.elem = [self createDOMElementWithText:run.string andFont:run.font andColor:run.color andBackgroundColor:run.bgcolor andUnderline:run.underline];
if (run.DOMactive && !run.DOMpatched)
continue;
if (!_glyphsFrames)
continue;
var loc = run._range.location - _runs[0]._range.location;
// Safety bounds check to protect against uninitialized/empty glyph frames or offsets
if (loc < 0 || loc >= _glyphsFrames.length || !_glyphsFrames[loc] || !_glyphsOffsets || loc >= _glyphsOffsets.length)
continue;
orig.x = _glyphsFrames[loc].origin.x + aPoint.x;
orig.y = _glyphsFrames[loc].origin.y + aPoint.y + _glyphsOffsets[loc];
if(run.elem || run.view)
{
if (run.elem)
{
run.elem.style.left = (orig.x) + "px";
run.elem.style.top = (orig.y) + "px";
}
if (run.view)
[run.view setFrameOrigin:orig];
if (!run.DOMactive)
{
if (run.view)
[_textContainer._textView addSubview:run.view];
if (run.elem)
_textContainer._textView._DOMElement.appendChild(run.elem);
}
run.DOMactive = YES;
}
run.DOMpatched = NO;
}
}
- (CPColor)backgroundColorForGlyphAtIndex:(unsigned)index
{
var run = _objectWithLocationInRange(_runs, index);
if (run)
return run.backgroundColor;
return [CPColor clearColor];
}
- (BOOL)isVisuallyIdenticalToFragment:(_CPLineFragment)newLineFragment
{
var newFragmentRuns= newLineFragment._runs,
oldFragmentRuns= _runs;
if (!oldFragmentRuns || !newFragmentRuns || oldFragmentRuns.length !== newFragmentRuns.length)
return NO;
var l = oldFragmentRuns.length;
for (var i = 0; i < l; i++)
{
if (newFragmentRuns[i].string !== oldFragmentRuns[i].string)
return NO;
if (!_RectEqualToRectHorizontally(newLineFragment._fragmentRect, _fragmentRect))
return NO;
if (newFragmentRuns[i].color !== oldFragmentRuns[i].color ||
newFragmentRuns[i].bgcolor !== oldFragmentRuns[i].bgcolor ||
newFragmentRuns[i].font !== oldFragmentRuns[i].font ||
newFragmentRuns[i].baselineOffset !== oldFragmentRuns[i].baselineOffset)
return NO;
var oldStyle = oldFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle],
newStyle = newFragmentRuns[i].paragraphStyle || [CPParagraphStyle defaultParagraphStyle];
if (![oldStyle isEqual:newStyle])
return NO;
}
return YES;
}
- (void)_relocateVerticallyByY:(double)verticalOffset rangeOffset:(unsigned)rangeOffset
{
var l = _runs.length;
_range.location += rangeOffset;
for (var i = 0; i < l; i++)
{
_runs[i]._range.location += rangeOffset;
if (verticalOffset)
{
if (_runs[i].view)
_runs[i].view._frame.origin.y += verticalOffset;
if (_runs[i].elem)
_runs[i].elem.top = (_runs[i].elem.top + verticalOffset) + 'px';
_runs[i].DOMpatched = YES;
}
}
if (!verticalOffset)
return NO;
_fragmentRect.origin.y += verticalOffset;
_usedRect.origin.y += verticalOffset;
var l = _glyphsFrames.length;
for (var i = 0; i < l ; i++)
{
_glyphsFrames[i].origin.y += verticalOffset;
}
}
@end
@implementation _CPTemporaryAttributes : CPObject
{
CPDictionary _attributes;
CPRange _range;
}
- (id)initWithRange:(CPRange)aRange attributes:(CPDictionary)attributes
{
if (self = [super init])
{
_attributes = attributes;
_range = CPMakeRangeCopy(aRange);
}
return self;
}
- (CPString)description
{
return [super description] +
"\n\t_range="+CPStringFromRange(_range) +
"\n\t_attributes="+[_attributes description];
}
@end