mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-10-25 17:45:36 +00:00
When CPLayoutManager retained and repositioned unchanged line fragments below an edit, `_CPLineFragment` incorrectly modified attachment view frames directly and assigned an unused property to text spans. Consequently, moved views remained visually stationary on screen because subsequent layout-pass `setFrameOrigin:` calls detected matching origins, causing asynchronously loaded elements (such as pasted tables combined with images) to render at incorrect offsets until forced by a scroll or resize event. * AppKit/CPTextView/CPLayoutManager.j: - Update attachment view relocation to correctly trigger `setFrameOrigin:`. - Shift rendered text spans via their style coordinate top properties. * Tests/Manual/TextEditor/WORKLIST.md: - Mark composite paste workflows (text containing both images and tables) as tested. Attachment views and text spans now immediately track their corresponding line fragments when text is inserted or modified above them.
1603 lines
47 KiB
Plaintext
1603 lines
47 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 "CGContext.j"
|
|
@import "CPTypesetter.j"
|
|
@import "CPFont.j"
|
|
@import "CPCompatibility.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;
|
|
CPRange _lastEditedRange;
|
|
}
|
|
|
|
|
|
// MARK: -
|
|
// 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];
|
|
_lastEditedRange = nil;
|
|
|
|
[_textStorage addLayoutManager:self];
|
|
}
|
|
|
|
|
|
// MARK: -
|
|
// 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 (CPDOMAvailable)
|
|
{
|
|
[self _cleanUpDOM];
|
|
}
|
|
|
|
_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;
|
|
|
|
// Ensure that the remaining lines we are attempting to rescue
|
|
// start after the end of the edited region.
|
|
if (newTargetLine < l && _lastEditedRange)
|
|
{
|
|
var firstRescuedLineNewLocation = _lineFragmentsForRescue[newTargetLine]._range.location + rangeOffset;
|
|
|
|
if (firstRescuedLineNewLocation < CPMaxRange(_lastEditedRange))
|
|
return NO;
|
|
}
|
|
|
|
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);
|
|
|
|
_lastEditedRange = CPMakeRangeCopy(charRange);
|
|
|
|
[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);
|
|
|
|
if (!rect)
|
|
rect = CGRectCreateCopy(correctedRect);
|
|
else
|
|
rect = CGRectUnion(rect, correctedRect);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
float _wordSpacing;
|
|
CGRect _fragmentRect;
|
|
CGRect _usedRect;
|
|
CGPoint _location;
|
|
CPRange _range;
|
|
CPTextContainer _textContainer;
|
|
CPMutableArray _runs;
|
|
}
|
|
|
|
// MARK: -
|
|
// 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 (!CPDOMAvailable) return nil;
|
|
|
|
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];
|
|
|
|
// A justified line widens its spaces; the typesetter has added the same width to their advancements.
|
|
if (_wordSpacing)
|
|
style.wordSpacing = _wordSpacing + "px";
|
|
|
|
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;
|
|
}
|
|
|
|
- (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;
|
|
_wordSpacing = 0.0;
|
|
_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;
|
|
|
|
// Align the run's baseline with the common line baseline (_location.y)
|
|
_glyphsOffsets[i] = _location.y - 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;
|
|
|
|
if (newLineFragment._wordSpacing !== _wordSpacing)
|
|
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)
|
|
{
|
|
// An attachment view is moved through setFrameOrigin:, which also moves it on the
|
|
// screen. Changing its frame directly would leave it in place, because the next draw's
|
|
// setFrameOrigin: would then find it already at the new origin and do nothing.
|
|
if (_runs[i].view)
|
|
{
|
|
var viewOrigin = [_runs[i].view frameOrigin];
|
|
|
|
[_runs[i].view setFrameOrigin:CGPointMake(viewOrigin.x, viewOrigin.y + verticalOffset)];
|
|
}
|
|
|
|
// A span that has been placed moves with its line. One that has not is placed when drawn.
|
|
if (_runs[i].elem && _runs[i].elem.style.top)
|
|
_runs[i].elem.style.top = (parseFloat(_runs[i].elem.style.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
|