Merge remote-tracking branch 'upstream/main' into Aristo3

This commit is contained in:
daboe01 committed 2026-10-24 22:00:51 +02:00
commit 3ad080b788
142 files changed
+14126 -3630

No files matched your search

+16 -8
View File
@@ -25,10 +25,10 @@
*/
@import "CPText.j"
@import "CPTextContainer.j"
@import "CGContext.j"
@import "CPTypesetter.j"
@import "CPFont.j"
@import "CPCompatibility.j"
@global _MakeRangeFromAbs
@global document
@@ -298,9 +298,10 @@ _oncontextmenuhandler = function () { return false; };
[_typesetter layoutGlyphsInLayoutManager:self startingAtGlyphIndex:startIndex maxNumberOfLineFragments:-1 nextGlyphIndex:nil];
#if PLATFORM(DOM)
[self _cleanUpDOM];
#endif
if (CPDOMAvailable)
{
[self _cleanUpDOM];
}
_isValidatingLayoutAndGlyphs = NO;
}
@@ -1124,6 +1125,7 @@ var _objectsInRange = function(aList, aRange)
BOOL _isInvalid;
BOOL _isLast;
float _wordSpacing;
CGRect _fragmentRect;
CGRect _usedRect;
CGPoint _location;
@@ -1146,7 +1148,8 @@ var _objectsInRange = function(aList, aRange)
if (!aString || aString.length === 0)
return nil;
#if PLATFORM(DOM)
if (!CPDOMAvailable) return nil;
var style,
span = document.createElement("span");
@@ -1160,6 +1163,10 @@ var _objectsInRange = function(aList, aRange)
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)
{
@@ -1197,9 +1204,6 @@ var _objectsInRange = function(aList, aRange)
span.textContent = aString;
return span;
#else
return nil;
#endif
}
- (id)initWithRange:(CPRange)aRange textContainer:(CPTextContainer)aContainer textStorage:(CPTextStorage)textStorage
@@ -1215,6 +1219,7 @@ var _objectsInRange = function(aList, aRange)
_range = CPMakeRangeCopy(aRange);
_textContainer = aContainer;
_isInvalid = NO;
_wordSpacing = 0.0;
_runs = [];
_glyphsFrames = [];
_glyphsOffsets = [];
@@ -1495,6 +1500,9 @@ var _objectsInRange = function(aList, aRange)
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++)
+36 -1
View File
@@ -106,6 +106,24 @@ var kDefaultTabInterval = 28.0;
// MARK: - CPParagraphStyle Implementation
/*
Returns YES if both arrays hold the same text lists, in the same order. A text list is
identified by the object itself, so the lists are compared by identity.
*/
var _CPParagraphStyleTextListsAreIdentical = function(someLists, otherLists)
{
var count = someLists ? someLists.length : 0;
if (count !== (otherLists ? otherLists.length : 0))
return NO;
for (var i = 0; i < count; i++)
if (someLists[i] !== otherLists[i])
return NO;
return YES;
};
var _sharedDefaultParagraphStyle = nil;
@implementation CPParagraphStyle : CPObject
@@ -124,6 +142,7 @@ var _sharedDefaultParagraphStyle = nil;
float _paragraphSpacingBefore @accessors(readonly, property=paragraphSpacingBefore);
float _defaultTabInterval @accessors(readonly, property=defaultTabInterval);
CPArray _tabStops @accessors(readonly, property=tabStops);
CPArray _textLists @accessors(readonly, property=textLists);
}
+ (CPParagraphStyle)defaultParagraphStyle
@@ -168,6 +187,8 @@ var _sharedDefaultParagraphStyle = nil;
[_tabStops addObject:[[CPTextTab alloc] initWithType:CPLeftTextAlignment
location:i * kDefaultTabInterval]];
}
_textLists = [];
}
return self;
}
@@ -190,6 +211,7 @@ var _sharedDefaultParagraphStyle = nil;
_paragraphSpacingBefore = [other paragraphSpacingBefore];
_defaultTabInterval = [other defaultTabInterval];
_tabStops = [[other tabStops] copy];
_textLists = [[other textLists] copy] || [];
}
return self;
}
@@ -229,6 +251,7 @@ var _sharedDefaultParagraphStyle = nil;
_paragraphSpacingBefore = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacingBefore"];
_defaultTabInterval = [aCoder decodeFloatForKey:@"CPParagraphStyleDefaultTabInterval"];
_tabStops = [aCoder decodeObjectForKey:@"CPParagraphStyleTabStops"];
_textLists = [aCoder decodeObjectForKey:@"CPParagraphStyleTextLists"] || [];
}
return self;
}
@@ -249,6 +272,9 @@ var _sharedDefaultParagraphStyle = nil;
[aCoder encodeFloat:_paragraphSpacingBefore forKey:@"CPParagraphStyleParagraphSpacingBefore"];
[aCoder encodeFloat:_defaultTabInterval forKey:@"CPParagraphStyleDefaultTabInterval"];
[aCoder encodeObject:_tabStops forKey:@"CPParagraphStyleTabStops"];
if ([_textLists count])
[aCoder encodeObject:_textLists forKey:@"CPParagraphStyleTextLists"];
}
// MARK: - Equality
@@ -265,7 +291,8 @@ var _sharedDefaultParagraphStyle = nil;
_tailIndent === [other tailIndent] &&
_firstLineHeadIndent === [other firstLineHeadIndent] &&
_lineBreakMode === [other lineBreakMode] &&
[_tabStops isEqualToArray:[other tabStops]];
[_tabStops isEqualToArray:[other tabStops]] &&
_CPParagraphStyleTextListsAreIdentical(_textLists, [other textLists]);
}
@end
@@ -377,6 +404,14 @@ var _sharedDefaultParagraphStyle = nil;
_tabStops = [newTabStops mutableCopy];
}
/*!
Sets the text lists that the paragraph belongs to, outermost first.
*/
- (void)setTextLists:(CPArray)someTextLists
{
_textLists = [someTextLists copy] || [];
}
- (id)copyWithZone:(CPZone)aZone
{
// Return an immutable copy
+504
View File
@@ -0,0 +1,504 @@
/*
* CPRulerMarker.j
* AppKit
*
* Created by Daniel Boehringer on 11/01/2014
* Copyright Daniel Boehringer 2014.
*
* 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 "CGGeometry.j"
@import "CPEvent.j"
@import "CPGraphicsContext.j"
@import "CPImage.j"
@class CPRulerView
@global CPApp
@global CPHorizontalRuler
/*!
Distance, in points, that the pointer must move beyond the ruler before a removable
marker is removed when it is released.
*/
var CPRulerMarkerRemovalDistance = 0.0;
/*!
@ingroup appkit
@class CPRulerMarker
A CPRulerMarker shows a location on a CPRulerView, such as a tab stop or a paragraph
indent. The marker is drawn by its ruler view as an image, positioned so that the
image's \c imageOrigin lies at the marker's location on the ruler's baseline. The
marker's location is expressed in the coordinate system of the ruler's client view.
The user can drag a movable marker along the ruler, and drag a removable marker off the
ruler to remove it. The ruler's client view is consulted through the CPRulerView client
methods before and after each change.
*/
@implementation CPRulerMarker : CPObject
{
CPRulerView _ruler;
float _markerLocation;
CPImage _image;
CGPoint _imageOrigin;
BOOL _isMovable;
BOOL _isRemovable;
BOOL _isDragging;
BOOL _isOffRuler;
id _representedObject;
float _trackingStartLocation;
float _trackingStartPoint;
float _trackingStartCrossPoint;
float _trackingCrossOffset;
BOOL _trackingIsAdding;
BOOL _trackingMoved;
}
/*!
Initializes a marker for \c aRulerView at \c aLocation, drawn with \c anImage. The
marker is associated with the ruler view, but not added to it. A new marker is movable
and not removable.
@param aRulerView the ruler view the marker belongs to
@param aLocation the marker's location, in the client view's coordinate system
@param anImage the image drawn for the marker
@param anImageOrigin the point in the image, measured from its top left corner, placed
at the marker's location on the ruler's baseline
@return the initialized marker
*/
- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation image:(CPImage)anImage imageOrigin:(CGPoint)anImageOrigin
{
self = [super init];
if (self)
{
_ruler = aRulerView;
_markerLocation = aLocation;
_image = anImage;
_imageOrigin = CGPointMakeCopy(anImageOrigin);
_isMovable = YES;
_isRemovable = NO;
_isDragging = NO;
_isOffRuler = NO;
}
return self;
}
// MARK: Getting the Ruler View
/*!
Returns the ruler view the marker belongs to.
*/
- (CPRulerView)ruler
{
return _ruler;
}
// MARK: Setting the Image
/*!
Returns the image drawn for the marker.
*/
- (CPImage)image
{
return _image;
}
/*!
Sets the image drawn for the marker.
@param anImage the new image
*/
- (void)setImage:(CPImage)anImage
{
if (_image === anImage)
return;
_image = anImage;
[_ruler setNeedsDisplay:YES];
}
/*!
Returns the point in the image, measured from its top left corner, that is placed at
the marker's location on the ruler's baseline.
*/
- (CGPoint)imageOrigin
{
return _imageOrigin;
}
/*!
Sets the point in the image, measured from its top left corner, that is placed at the
marker's location on the ruler's baseline.
@param aPoint the new image origin
*/
- (void)setImageOrigin:(CGPoint)aPoint
{
_imageOrigin = CGPointMakeCopy(aPoint);
[_ruler setNeedsDisplay:YES];
}
/*!
Returns the rectangle that the marker's image occupies, in the ruler view's coordinate
system.
*/
- (CGRect)imageRectInRuler
{
var imageSize = [_image size] || CGSizeMakeZero(),
point = [_ruler _rulerPointForLocation:_markerLocation],
baseline = [_ruler baselineLocation];
// Whole-point positions keep every marker image sharp and identical, wherever it lies on the ruler.
// While the user drags the marker outward, it follows the pointer across the ruler.
var crossPosition = baseline + (_isDragging ? _trackingCrossOffset : 0.0);
if ([_ruler orientation] === CPHorizontalRuler)
return CGRectMake(ROUND(point - _imageOrigin.x), ROUND(crossPosition - _imageOrigin.y), imageSize.width, imageSize.height);
return CGRectMake(ROUND(crossPosition - _imageOrigin.x), ROUND(point - _imageOrigin.y), imageSize.width, imageSize.height);
}
/*!
Returns the extent of the marker's image on the marker side of the ruler's baseline:
above the baseline on a horizontal ruler, and to its left on a vertical ruler.
*/
- (float)thicknessRequiredInRuler
{
return [_ruler orientation] === CPHorizontalRuler ? _imageOrigin.y : _imageOrigin.x;
}
// MARK: Setting Movability
/*!
Returns \c YES if the user can drag the marker along the ruler.
*/
- (BOOL)isMovable
{
return _isMovable;
}
/*!
Sets whether the user can drag the marker along the ruler.
@param shouldBeMovable \c YES to let the user move the marker
*/
- (void)setMovable:(BOOL)shouldBeMovable
{
_isMovable = shouldBeMovable;
}
/*!
Returns \c YES if the user can remove the marker by dragging it off the ruler.
*/
- (BOOL)isRemovable
{
return _isRemovable;
}
/*!
Sets whether the user can remove the marker by dragging it off the ruler.
@param shouldBeRemovable \c YES to let the user remove the marker
*/
- (void)setRemovable:(BOOL)shouldBeRemovable
{
_isRemovable = shouldBeRemovable;
}
// MARK: Setting the Location
/*!
Returns the marker's location, in the client view's coordinate system.
*/
- (float)markerLocation
{
return _markerLocation;
}
/*!
Sets the marker's location, in the client view's coordinate system.
@param aLocation the new location
*/
- (void)setMarkerLocation:(float)aLocation
{
if (_markerLocation === aLocation)
return;
_markerLocation = aLocation;
[_ruler setNeedsDisplay:YES];
}
// MARK: Setting the Represented Object
/*!
Returns the object the marker represents, such as a CPTextTab.
*/
- (id)representedObject
{
return _representedObject;
}
/*!
Sets the object the marker represents.
@param anObject the represented object
*/
- (void)setRepresentedObject:(id)anObject
{
_representedObject = anObject;
}
// MARK: Drawing and Event Handling
/*!
Returns \c YES while the user is dragging the marker.
*/
- (BOOL)isDragging
{
return _isDragging;
}
/*!
Draws the marker's image if it lies within \c aRect, in the ruler view's coordinate
system. While a removable marker is dragged off the ruler, it is drawn faded.
@param aRect the rectangle being drawn
*/
- (void)drawRect:(CGRect)aRect
{
if (!_image || [_image loadStatus] !== CPImageLoadStatusCompleted)
return;
var imageRect = [self imageRectInRuler];
if (!CGRectIntersectsRect(imageRect, aRect))
return;
var context = [[CPGraphicsContext currentContext] graphicsPort];
CGContextSaveGState(context);
// Off the ruler's outer edge the marker follows the pointer and is clipped by the ruler.
// Off its inner edge it stays on its line, so it is drawn faded instead.
if (_isOffRuler && _trackingCrossOffset === 0.0)
CGContextSetAlpha(context, 0.4);
CGContextDrawImage(context, imageRect, _image);
CGContextRestoreGState(context);
}
/*!
Starts tracking the user's manipulation of the marker, from the mouse-down event
\c anEvent. Tracking continues until the mouse button is released.
If \c isAdding is \c YES, the marker is not yet in the ruler: it is added when the user
releases it over the ruler, if the client view agrees. Otherwise the marker is moved,
and removed if it is removable and released away from the ruler.
@param anEvent the mouse-down event that started the manipulation
@param isAdding \c YES if the marker is being added to the ruler
@return \c YES if tracking started, or \c NO if the marker cannot be moved or removed,
or the client view refused
*/
- (BOOL)trackMouse:(CPEvent)anEvent adding:(BOOL)isAdding
{
if (!isAdding && !_isMovable && !_isRemovable)
return NO;
if (isAdding && ![_ruler _clientShouldAddMarker:self])
return NO;
if (!isAdding && _isMovable && ![_ruler _clientShouldMoveMarker:self])
return NO;
_trackingIsAdding = isAdding;
_trackingMoved = NO;
_trackingStartLocation = _markerLocation;
_trackingStartPoint = [self _axisPointForEvent:anEvent];
_trackingStartCrossPoint = [self _crossPointForEvent:anEvent];
_trackingCrossOffset = 0.0;
_isDragging = YES;
_isOffRuler = NO;
if (isAdding)
{
_markerLocation = [_ruler _clientWillAddMarker:self atLocation:_markerLocation];
[_ruler _setTrackedMarker:self];
}
[_ruler setNeedsDisplay:YES];
[CPApp setTarget:self
selector:@selector(_trackMouseWithEvent:)
forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask
untilDate:nil
inMode:nil
dequeue:YES];
return YES;
}
/* @ignore */
- (void)_trackMouseWithEvent:(CPEvent)anEvent
{
var type = [anEvent type];
if (type === CPLeftMouseDragged)
{
_trackingMoved = YES;
_isOffRuler = [self _isEventOffRuler:anEvent] && (_trackingIsAdding || _isRemovable);
// As in Cocoa, the marker stays on its line while the pointer is on the ruler, and follows
// the pointer out over the ruler's outer edge, where the ruler clips it.
_trackingCrossOffset = (_trackingIsAdding || _isRemovable) ? MIN(0.0, [self _crossPointForEvent:anEvent] - _trackingStartCrossPoint) : 0.0;
if (_trackingIsAdding || _isMovable)
{
var proposedLocation = _trackingStartLocation + [self _axisPointForEvent:anEvent] - _trackingStartPoint;
_markerLocation = _trackingIsAdding ? [_ruler _clientWillAddMarker:self atLocation:proposedLocation]
: [_ruler _clientWillMoveMarker:self toLocation:proposedLocation];
}
[_ruler setNeedsDisplay:YES];
[CPApp setTarget:self
selector:@selector(_trackMouseWithEvent:)
forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask
untilDate:nil
inMode:nil
dequeue:YES];
return;
}
if (type !== CPLeftMouseUp)
return;
var wasOffRuler = _isOffRuler;
_trackingCrossOffset = 0.0;
_isDragging = NO;
_isOffRuler = NO;
if (_trackingIsAdding)
{
[_ruler _setTrackedMarker:nil];
if (!wasOffRuler)
{
[_ruler addMarker:self];
[_ruler _clientDidAddMarker:self];
}
}
else if (wasOffRuler && [_ruler _clientShouldRemoveMarker:self])
{
[_ruler removeMarker:self];
[_ruler _clientDidRemoveMarker:self];
}
else if (_trackingMoved)
{
if (wasOffRuler)
_markerLocation = _trackingStartLocation;
if (_markerLocation !== _trackingStartLocation)
[_ruler _clientDidMoveMarker:self];
}
[_ruler setNeedsDisplay:YES];
}
/* @ignore */
- (float)_crossPointForEvent:(CPEvent)anEvent
{
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil];
return [_ruler orientation] === CPHorizontalRuler ? point.y : point.x;
}
/* @ignore */
- (float)_axisPointForEvent:(CPEvent)anEvent
{
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil];
return [_ruler orientation] === CPHorizontalRuler ? point.x : point.y;
}
/* @ignore */
- (BOOL)_isEventOffRuler:(CPEvent)anEvent
{
// Only distance across the ruler counts. Dragging past either end of the ruler moves the marker.
var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil],
bounds = [_ruler bounds];
if ([_ruler orientation] === CPHorizontalRuler)
return point.y < CGRectGetMinY(bounds) - CPRulerMarkerRemovalDistance || point.y > CGRectGetMaxY(bounds) + CPRulerMarkerRemovalDistance;
return point.x < CGRectGetMinX(bounds) - CPRulerMarkerRemovalDistance || point.x > CGRectGetMaxX(bounds) + CPRulerMarkerRemovalDistance;
}
@end
var CPRulerMarkerRulerKey = @"CPRulerMarkerRulerKey",
CPRulerMarkerLocationKey = @"CPRulerMarkerLocationKey",
CPRulerMarkerImageKey = @"CPRulerMarkerImageKey",
CPRulerMarkerImageOriginKey = @"CPRulerMarkerImageOriginKey",
CPRulerMarkerMovableKey = @"CPRulerMarkerMovableKey",
CPRulerMarkerRemovableKey = @"CPRulerMarkerRemovableKey",
CPRulerMarkerRepresentedObjectKey = @"CPRulerMarkerRepresentedObjectKey";
@implementation CPRulerMarker (CPCoding)
/*!
Initializes a marker from an archive.
*/
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super init];
if (self)
{
_ruler = [aCoder decodeObjectForKey:CPRulerMarkerRulerKey];
_markerLocation = [aCoder decodeFloatForKey:CPRulerMarkerLocationKey];
_image = [aCoder decodeObjectForKey:CPRulerMarkerImageKey];
_imageOrigin = [aCoder decodePointForKey:CPRulerMarkerImageOriginKey];
_isMovable = [aCoder decodeBoolForKey:CPRulerMarkerMovableKey];
_isRemovable = [aCoder decodeBoolForKey:CPRulerMarkerRemovableKey];
_representedObject = [aCoder decodeObjectForKey:CPRulerMarkerRepresentedObjectKey];
_isDragging = NO;
_isOffRuler = NO;
_trackingCrossOffset = 0.0;
}
return self;
}
/*!
Archives the marker. The ruler is archived conditionally.
*/
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeConditionalObject:_ruler forKey:CPRulerMarkerRulerKey];
[aCoder encodeFloat:_markerLocation forKey:CPRulerMarkerLocationKey];
[aCoder encodeObject:_image forKey:CPRulerMarkerImageKey];
[aCoder encodePoint:_imageOrigin forKey:CPRulerMarkerImageOriginKey];
[aCoder encodeBool:_isMovable forKey:CPRulerMarkerMovableKey];
[aCoder encodeBool:_isRemovable forKey:CPRulerMarkerRemovableKey];
[aCoder encodeObject:_representedObject forKey:CPRulerMarkerRepresentedObjectKey];
}
@end
+367
View File
@@ -0,0 +1,367 @@
/*
* CPTextAttachment.j
* AppKit
*
* Created by David Richardson on 25/10/2026.
* Copyright David Richardson 2026.
*
* 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 <Foundation/CPData.j>
@import <Foundation/CPNotificationCenter.j>
@import "CPImage.j"
@import "CPImageView.j"
/*
Posted when an attachment's displayed size changes, for example when its image has loaded.
A text view that contains the attachment lays its text out again.
*/
_CPTextAttachmentDidChangeSizeNotification = @"_CPTextAttachmentDidChangeSizeNotification";
/*!
@ingroup appkit
@class CPTextAttachment
A CPTextAttachment holds an image in rich text, as NSTextAttachment does in Cocoa. The text
holds the attachment as one attachment character (CPAttachmentCharacter), whose
CPAttachmentAttributeName attribute is the attachment. The attachment is edited, copied and
undone as that one character.
The attachment keeps the image's data and type, and shows the image in a view. As in TextKit
2, an attachment is drawn by a view, not by a cell. The text system lays the view out as the
character, at the view's size, with its bottom on the baseline. That size is the attachment's
bounds, or the image's natural size, scaled down to fit the width of the text, as in TextEdit.
*/
@implementation CPTextAttachment : CPObject
{
CPData _contents;
CPString _fileType;
CPImage _image;
CGRect _bounds;
CPImageView _imageView;
float _maximumWidth;
}
/*!
Initialises an attachment with image data.
@param contentData the image's data
@param aType the image's type, as a MIME type such as \c @"image/png", or a uniform type
identifier such as \c @"public.png"
*/
- (id)initWithData:(CPData)contentData ofType:(CPString)aType
{
if (self = [super init])
{
_contents = contentData;
_fileType = aType;
_bounds = CGRectMakeZero();
_maximumWidth = 0.0;
}
return self;
}
- (id)init
{
return [self initWithData:nil ofType:nil];
}
/* @ignore
Returns an attachment made from a data URL, such as the browser's FileReader produces.
*/
+ (CPTextAttachment)_attachmentWithDataURL:(CPString)aDataURL
{
var match = /^data:([^;,]+)(;base64)?,(.*)$/.exec(aDataURL || "");
if (!match)
return nil;
var data = match[2] ? [CPData dataWithBase64:match[3]] : [CPData dataWithRawString:decodeURIComponent(match[3])];
return [[self alloc] initWithData:data ofType:match[1]];
}
/*!
Returns the image's data.
*/
- (CPData)contents
{
return _contents;
}
/*!
Sets the image's data. The image is made again from the data.
*/
- (void)setContents:(CPData)contentData
{
_contents = contentData;
[self _setImage:nil];
}
/*!
Returns the image's type.
*/
- (CPString)fileType
{
return _fileType;
}
/*!
Sets the image's type.
*/
- (void)setFileType:(CPString)aType
{
_fileType = aType;
}
/*!
Returns the image. It is made from the attachment's data the first time it is needed.
*/
- (CPImage)image
{
if (!_image && _contents)
[self _setImage:[[CPImage alloc] initWithContentsOfFile:[self _dataURL]]];
return _image;
}
/*!
Sets the image. An image set this way has no data of its own unless it was made from a data
URL, so an attachment that must be archived or copied should be made with
-initWithData:ofType:.
*/
- (void)setImage:(CPImage)anImage
{
[self _setImage:anImage];
}
/*!
Returns the attachment's bounds in the text. With a zero size, the attachment takes the
image's natural size.
*/
- (CGRect)bounds
{
return CGRectMakeCopy(_bounds);
}
/*!
Sets the attachment's bounds in the text.
*/
- (void)setBounds:(CGRect)aRect
{
_bounds = CGRectMakeCopy(aRect);
[self _updateViewSize];
}
// MARK: Display
/* @ignore
Returns the view that shows the attachment in the text. The text system lays it out as the
attachment character.
*/
- (CPView)_attachmentView
{
if (!_imageView)
{
_imageView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
[_imageView setImageScaling:CPImageScaleAxesIndependently];
[_imageView setImage:[self image]];
[self _updateViewSize];
}
return _imageView;
}
/* @ignore
Sets the widest the attachment may be shown, which is the width of the text. Returns YES if
the displayed size changes.
*/
- (BOOL)_setMaximumWidth:(float)aWidth
{
if (_maximumWidth === aWidth)
return NO;
_maximumWidth = aWidth;
return [self _updateViewSize];
}
/* @ignore
Returns the size at which the attachment is shown: its bounds, or the image's natural size,
scaled down in proportion to fit the maximum width. The size is zero until the image has
loaded.
*/
- (CGSize)_displaySize
{
var size = CGSizeMakeCopy(_bounds.size);
if (!size.width || !size.height)
{
var image = [self image];
if (!image || [image loadStatus] !== CPImageLoadStatusCompleted)
return CGSizeMakeZero();
size = CGSizeMakeCopy([image size]);
}
if (_maximumWidth > 0.0 && size.width > _maximumWidth)
{
size.height = ROUND(size.height * _maximumWidth / size.width);
size.width = _maximumWidth;
}
return size;
}
/* @ignore
Gives the view the displayed size. Returns YES if the size changes.
*/
- (BOOL)_updateViewSize
{
if (!_imageView)
return NO;
var size = [self _displaySize];
if (CGSizeEqualToSize(size, [_imageView frameSize]))
return NO;
[_imageView setFrameSize:size];
return YES;
}
/* @ignore
Sets the image, and waits for it to load when it has not yet loaded.
*/
- (void)_setImage:(CPImage)anImage
{
if (_image)
[[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:_image];
_image = anImage;
if (_image && [_image loadStatus] !== CPImageLoadStatusCompleted)
{
[[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_imageDidLoad:) name:CPImageDidLoadNotification object:_image];
[_image load];
}
if (_imageView)
{
[_imageView setImage:_image];
if ([self _updateViewSize])
[[CPNotificationCenter defaultCenter] postNotificationName:_CPTextAttachmentDidChangeSizeNotification object:self];
}
}
/* @ignore
The image's natural size is known once it has loaded, so the view takes its size, and the
text that contains the attachment is laid out again.
*/
- (void)_imageDidLoad:(CPNotification)aNotification
{
[[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:_image];
if (_imageView)
[_imageView setImage:_image];
if ([self _updateViewSize])
[[CPNotificationCenter defaultCenter] postNotificationName:_CPTextAttachmentDidChangeSizeNotification object:self];
}
/* @ignore
Returns the attachment's data as a data URL, which the browser loads as an image.
*/
- (CPString)_dataURL
{
return "data:" + [self _MIMEType] + ";base64," + [_contents base64];
}
/* @ignore
Returns the image's MIME type. A uniform type identifier is mapped to its MIME type.
*/
- (CPString)_MIMEType
{
var type = _fileType || @"image/png";
if (type.indexOf("/") !== -1)
return type;
switch (type)
{
case @"public.jpeg":
return @"image/jpeg";
case @"com.compuserve.gif":
return @"image/gif";
case @"public.svg-image":
return @"image/svg+xml";
case @"org.webmproject.webp":
return @"image/webp";
default:
return @"image/png";
}
}
@end
var CPTextAttachmentContentsKey = @"CPTextAttachmentContents",
CPTextAttachmentFileTypeKey = @"CPTextAttachmentFileType",
CPTextAttachmentBoundsKey = @"CPTextAttachmentBounds";
@implementation CPTextAttachment (CPCoding)
/*!
Decodes the attachment. The image is made again from the archived data.
*/
- (id)initWithCoder:(CPCoder)aCoder
{
var contentsString = [aCoder decodeObjectForKey:CPTextAttachmentContentsKey];
self = [self initWithData:contentsString ? [CPData dataWithBase64:contentsString] : nil
ofType:[aCoder decodeObjectForKey:CPTextAttachmentFileTypeKey]];
if (self && [aCoder containsValueForKey:CPTextAttachmentBoundsKey])
_bounds = [aCoder decodeRectForKey:CPTextAttachmentBoundsKey];
return self;
}
/*!
Encodes the attachment's data, type and bounds. The view is not encoded.
*/
- (void)encodeWithCoder:(CPCoder)aCoder
{
if (_contents)
[aCoder encodeObject:[_contents base64] forKey:CPTextAttachmentContentsKey];
if (_fileType)
[aCoder encodeObject:_fileType forKey:CPTextAttachmentFileTypeKey];
if (!CGRectIsEmpty(_bounds))
[aCoder encodeRect:_bounds forKey:CPTextAttachmentBoundsKey];
}
@end
+18 -5
View File
@@ -112,7 +112,8 @@ CPLineMovesUp = 4;
- (void)_init
{
_lineFragmentPadding = 0.0;
// Cocoa's default: room at each end of a line fragment, so that text does not touch the container's edges.
_lineFragmentPadding = 5.0;
_layoutManager = [[CPLayoutManager alloc] init];
[_layoutManager addTextContainer:self];
@@ -129,6 +130,9 @@ CPLineMovesUp = 4;
if (oldSize.width != _size.width)
{
// Image attachments fit the new width before the text is laid out again.
[_textView _setTextAttachmentsMaximumWidth:MAX(0.0, _size.width - 2.0 * _lineFragmentPadding)];
_inResizing = YES;
[_layoutManager invalidateLayoutForCharacterRange:CPMakeRange(0, [[_layoutManager textStorage] length])
isSoft:NO
@@ -193,13 +197,16 @@ CPLineMovesUp = 4;
- (void)textViewFrameChanged:(CPNotification)aNotification
{
var newSize = CGSizeMake(_size.width, _size.height);
// As in Cocoa, a tracking container fills the text view inside its text container inset on both sides.
var newSize = CGSizeMake(_size.width, _size.height),
frameSize = [_textView frame].size,
inset = [_textView textContainerInset];
if (_widthTracksTextView)
newSize.width = [_textView frame].size.width;
newSize.width = MAX(0.0, frameSize.width - 2.0 * inset.width);
if (_heightTracksTextView)
newSize.height = [_textView frame].size.height;
newSize.height = MAX(0.0, frameSize.height - 2.0 * inset.height);
[self setContainerSize:newSize];
}
@@ -271,7 +278,8 @@ CPLineMovesUp = 4;
var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
CPTextContainerLayoutManagerKey = @"CPTextContainerLayoutManagerKey",
CPTextContainerWidthTracksTextViewKey = @"CPTextContainerWidthTracksTextViewKey",
CPTextContainerHeightTracksTextViewKey = @"CPTextContainerHeightTracksTextViewKey";
CPTextContainerHeightTracksTextViewKey = @"CPTextContainerHeightTracksTextViewKey",
CPTextContainerLineFragmentPaddingKey = @"CPTextContainerLineFragmentPaddingKey";
@implementation CPTextContainer (CPCoding)
@@ -290,6 +298,10 @@ var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
_widthTracksTextView = [aCoder decodeBoolForKey:CPTextContainerWidthTracksTextViewKey];
_heightTracksTextView = [aCoder decodeBoolForKey:CPTextContainerHeightTracksTextViewKey];
// Archives made before the padding was archived keep the default from _init.
if ([aCoder containsValueForKey:CPTextContainerLineFragmentPaddingKey])
_lineFragmentPadding = [aCoder decodeFloatForKey:CPTextContainerLineFragmentPaddingKey];
}
return self;
@@ -301,6 +313,7 @@ var CPTextContainerSizeKey = @"CPTextContainerSizeKey",
[aCoder encodeObject:_layoutManager forKey:CPTextContainerLayoutManagerKey];
[aCoder encodeBool:_widthTracksTextView forKey:CPTextContainerWidthTracksTextViewKey];
[aCoder encodeBool:_heightTracksTextView forKey:CPTextContainerHeightTracksTextViewKey];
[aCoder encodeFloat:_lineFragmentPadding forKey:CPTextContainerLineFragmentPaddingKey];
}
@end
+403
View File
@@ -0,0 +1,403 @@
/*
* CPTextList.j
* AppKit
*
* Created by David Richardson on 24/10/2026.
* Copyright David Richardson 2026.
*
* 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 <Foundation/CPAttributedString.j>
@import "CPParagraphStyle.j"
/*
CPTextListOptions
*/
CPTextListPrependEnclosingMarker = 1 << 0;
/*
CPTextListMarkerFormat
*/
CPTextListMarkerBox = @"{box}";
CPTextListMarkerCheck = @"{check}";
CPTextListMarkerCircle = @"{circle}";
CPTextListMarkerDiamond = @"{diamond}";
CPTextListMarkerDisc = @"{disc}";
CPTextListMarkerHyphen = @"{hyphen}";
CPTextListMarkerSquare = @"{square}";
CPTextListMarkerLowercaseHexadecimal = @"{lower-hexadecimal}";
CPTextListMarkerUppercaseHexadecimal = @"{upper-hexadecimal}";
CPTextListMarkerOctal = @"{octal}";
CPTextListMarkerLowercaseAlpha = @"{lower-alpha}";
CPTextListMarkerUppercaseAlpha = @"{upper-alpha}";
CPTextListMarkerLowercaseLatin = @"{lower-latin}";
CPTextListMarkerUppercaseLatin = @"{upper-latin}";
CPTextListMarkerLowercaseRoman = @"{lower-roman}";
CPTextListMarkerUppercaseRoman = @"{upper-roman}";
CPTextListMarkerDecimal = @"{decimal}";
/*
The characters that the unordered marker formats stand for.
*/
var CPTextListBulletCharacters = {
"box": "□",
"check": "✓",
"circle": "◦",
"diamond": "◆",
"disc": "•",
"hyphen": "⁃",
"square": "▪"
},
CPTextListPlaceholderPattern = /\{([a-z-]+)\}/;
/*
Returns anItemNumber as a letter sequence: 1 is "a", 26 is "z", 27 is "aa".
*/
var _CPTextListAlphaString = function(anItemNumber)
{
var result = "";
while (anItemNumber > 0)
{
anItemNumber--;
result = String.fromCharCode(97 + anItemNumber % 26) + result;
anItemNumber = Math.floor(anItemNumber / 26);
}
return result;
};
/*
Returns anItemNumber as a lowercase Roman numeral.
*/
var _CPTextListRomanString = function(anItemNumber)
{
var values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
numerals = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"],
result = "";
for (var i = 0; i < values.length; i++)
{
while (anItemNumber >= values[i])
{
result += numerals[i];
anItemNumber -= values[i];
}
}
return result;
};
/*!
@ingroup appkit
@class CPTextList
A CPTextList describes one list in rich text: the format of its markers, its options and the
number of its first item. A paragraph belongs to the lists in its paragraph style's
\c textLists, outermost first. As in Cocoa, the markers are ordinary text at the start of
each item: a tab, the marker and another tab, placed by the paragraph style's indents and
tab stops. A list is identified by the object itself, so the paragraphs of one list share
the same CPTextList.
*/
@implementation CPTextList : CPObject
{
CPString _markerFormat;
unsigned _listOptions;
int _startingItemNumber;
}
/*!
Initialises a list.
@param aFormat the marker format: one of the CPTextListMarker constants, optionally with
text around it, such as \c @"({decimal})"
@param aMask the list options, such as CPTextListPrependEnclosingMarker
*/
- (id)initWithMarkerFormat:(CPString)aFormat options:(unsigned)aMask
{
return [self initWithMarkerFormat:aFormat options:aMask startingItemNumber:1];
}
/*!
Initialises a list whose first item has the number anItemNumber.
*/
- (id)initWithMarkerFormat:(CPString)aFormat options:(unsigned)aMask startingItemNumber:(int)anItemNumber
{
if (self = [super init])
{
_markerFormat = aFormat || CPTextListMarkerDisc;
_listOptions = aMask;
_startingItemNumber = anItemNumber;
}
return self;
}
/*!
Returns the marker format.
*/
- (CPString)markerFormat
{
return _markerFormat;
}
/*!
Returns the list options.
*/
- (unsigned)listOptions
{
return _listOptions;
}
/*!
Returns the number of the list's first item. The default is 1.
*/
- (int)startingItemNumber
{
return _startingItemNumber;
}
/*!
Sets the number of the list's first item.
*/
- (void)setStartingItemNumber:(int)anItemNumber
{
_startingItemNumber = anItemNumber;
}
/*!
Returns \c YES if the markers are numbers or letters, and \c NO if they are bullets.
*/
- (BOOL)isOrdered
{
var match = _markerFormat.match(CPTextListPlaceholderPattern);
return !!match && !CPTextListBulletCharacters.hasOwnProperty(match[1]);
}
/*!
Returns the marker for the item with the number anItemNumber, without the tabs around it.
The marker format's placeholder is replaced by the bullet or the formatted number, and the
text around the placeholder is kept.
*/
- (CPString)markerForItemNumber:(int)anItemNumber
{
var match = _markerFormat.match(CPTextListPlaceholderPattern);
if (!match)
return _markerFormat;
var name = match[1],
marker;
if (CPTextListBulletCharacters.hasOwnProperty(name))
marker = CPTextListBulletCharacters[name];
else
{
switch (name)
{
case "lower-hexadecimal":
marker = anItemNumber.toString(16);
break;
case "upper-hexadecimal":
marker = anItemNumber.toString(16).toUpperCase();
break;
case "octal":
marker = anItemNumber.toString(8);
break;
case "lower-alpha":
case "lower-latin":
marker = anItemNumber > 0 ? _CPTextListAlphaString(anItemNumber) : String(anItemNumber);
break;
case "upper-alpha":
case "upper-latin":
marker = anItemNumber > 0 ? _CPTextListAlphaString(anItemNumber).toUpperCase() : String(anItemNumber);
break;
case "lower-roman":
marker = anItemNumber > 0 ? _CPTextListRomanString(anItemNumber) : String(anItemNumber);
break;
case "upper-roman":
marker = anItemNumber > 0 ? _CPTextListRomanString(anItemNumber).toUpperCase() : String(anItemNumber);
break;
default:
marker = String(anItemNumber);
}
}
return _markerFormat.replace(CPTextListPlaceholderPattern, marker);
}
- (CPString)description
{
return [super description] + " markerFormat=" + _markerFormat + " startingItemNumber=" + _startingItemNumber;
}
@end
var CPTextListMarkerFormatKey = @"CPTextListMarkerFormat",
CPTextListOptionsKey = @"CPTextListOptions",
CPTextListStartingItemNumberKey = @"CPTextListStartingItemNumber";
@implementation CPTextList (CPCoding)
- (id)initWithCoder:(CPCoder)aCoder
{
if (self = [super init])
{
_markerFormat = [aCoder decodeObjectForKey:CPTextListMarkerFormatKey] || CPTextListMarkerDisc;
_listOptions = [aCoder decodeIntForKey:CPTextListOptionsKey];
_startingItemNumber = [aCoder containsValueForKey:CPTextListStartingItemNumberKey] ? [aCoder decodeIntForKey:CPTextListStartingItemNumberKey] : 1;
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeObject:_markerFormat forKey:CPTextListMarkerFormatKey];
[aCoder encodeInt:_listOptions forKey:CPTextListOptionsKey];
[aCoder encodeInt:_startingItemNumber forKey:CPTextListStartingItemNumberKey];
}
@end
/*
Returns YES if the character code separates paragraphs.
*/
var _CPTextListIsParagraphSeparator = function(aCharCode)
{
return aCharCode === 10 || aCharCode === 13 || aCharCode === 0x2029;
};
@implementation CPAttributedString (CPTextListAdditions)
/* @ignore
Returns the range of the paragraph containing anIndex, including its paragraph separator.
*/
- (CPRange)_paragraphRangeForIndex:(unsigned)anIndex
{
var string = [self string],
length = string.length,
start = MIN(anIndex, length),
end = start;
while (start > 0 && !_CPTextListIsParagraphSeparator(string.charCodeAt(start - 1)))
start--;
while (end < length && !_CPTextListIsParagraphSeparator(string.charCodeAt(end)))
end++;
if (end < length)
end++;
return CPMakeRange(start, end - start);
}
/* @ignore
Returns the text lists of the paragraph that starts at aParagraphStart, outermost first.
*/
- (CPArray)_textListsOfParagraphAtIndex:(unsigned)aParagraphStart
{
if (aParagraphStart >= [self length])
return [];
var paragraphStyle = [self attribute:CPParagraphStyleAttributeName atIndex:aParagraphStart effectiveRange:nil];
return [paragraphStyle textLists] || [];
}
/*!
Returns the range of the paragraphs that belong to aList, around anIndex. The paragraphs
are contiguous: the range ends at the first paragraph on either side that does not belong
to the list, at any level. Returns a range with the location CPNotFound if the paragraph
at anIndex does not belong to aList.
*/
- (CPRange)rangeOfTextList:(CPTextList)aList atIndex:(unsigned)anIndex
{
var paragraphRange = [self _paragraphRangeForIndex:anIndex];
if (![[self _textListsOfParagraphAtIndex:paragraphRange.location] containsObjectIdenticalTo:aList])
return CPMakeRange(CPNotFound, 0);
var start = paragraphRange.location,
end = CPMaxRange(paragraphRange),
length = [self length];
while (start > 0)
{
var previousRange = [self _paragraphRangeForIndex:start - 1];
if (![[self _textListsOfParagraphAtIndex:previousRange.location] containsObjectIdenticalTo:aList])
break;
start = previousRange.location;
}
while (end < length)
{
var nextRange = [self _paragraphRangeForIndex:end];
if (![[self _textListsOfParagraphAtIndex:nextRange.location] containsObjectIdenticalTo:aList])
break;
end = CPMaxRange(nextRange);
}
return CPMakeRange(start, end - start);
}
/*!
Returns the number of the item of aList at anIndex. The items of a list are the paragraphs
whose innermost list it is; paragraphs of lists nested in it do not count. For a paragraph
of a nested list, the result is the number of the enclosing item. Returns 0 if the
paragraph at anIndex does not belong to aList.
*/
- (int)itemNumberInTextList:(CPTextList)aList atIndex:(unsigned)anIndex
{
var listRange = [self rangeOfTextList:aList atIndex:anIndex];
if (listRange.location === CPNotFound)
return 0;
var target = [self _paragraphRangeForIndex:anIndex].location,
itemNumber = [aList startingItemNumber] - 1,
location = listRange.location;
while (location <= target)
{
var paragraphRange = [self _paragraphRangeForIndex:location];
if ([[self _textListsOfParagraphAtIndex:location] lastObject] === aList)
itemNumber++;
if (!paragraphRange.length)
break;
location = CPMaxRange(paragraphRange);
}
return MAX(itemNumber, [aList startingItemNumber]);
}
@end
+13 -5
View File
@@ -26,6 +26,7 @@
@import "CPText.j"
@import "CPFont.j"
@import "CPTextAttachment.j"
@class CPLayoutManager;
@@ -263,16 +264,23 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1,
}
/*!
Returns an instance of CPTextStorage that contains the provided instance of CPView.
This can be used to insert arbitrary views into the text. These views are treated as individual characters during editing.
This works only with views that conform to the CPCoding protocol
Returns an instance of CPTextStorage that contains one attachment character for anAttachment.
The attachment is treated as an individual character during editing.
anAttachment is either a CPTextAttachment, as in Cocoa, or a view. A CPTextAttachment is
stored as the character's CPAttachmentAttributeName attribute, and its view is laid out as
the character. A view is inserted into the text directly; this works only with views that
conform to the CPCoding protocol.
*/
+ (id)attributedStringWithAttachment:(CPView)someView
+ (id)attributedStringWithAttachment:(id)anAttachment
{
var result = [[self alloc] initWithString:_CPAttachmentCharacterAsString];
[result setAttributes:@{_CPAttachmentView:someView} range:CPMakeRange(0, 1)];
if ([anAttachment isKindOfClass:[CPTextAttachment class]])
[result setAttributes:@{CPAttachmentAttributeName:anAttachment, _CPAttachmentView:[anAttachment _attachmentView]} range:CPMakeRange(0, 1)];
else
[result setAttributes:@{_CPAttachmentView:anAttachment} range:CPMakeRange(0, 1)];
return result;
}
File diff suppressed because it is too large. Load diff
+97 -12
View File
@@ -49,7 +49,23 @@ CPTypesetterParagraphBreakAction = 1 << 4;
CPTypesetterContainerBreakAction = 1 << 5;
var CPSystemTypesetterFactory,
_sharedSimpleTypesetter;
_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
@@ -170,10 +186,32 @@ var CPSystemTypesetterFactory,
advancements:(CPArray)advancements
lineCount:(unsigned)lineCount
sameLine:(BOOL)sameLine
wordWrapped:(BOOL)wordWrapped
rightMargin:(float)rightMargin
{
var myX = 0,
rect = CGRectMake(lineOrigin.x, lineOrigin.y, _lineWidth, _lineHeight),
containerSize = aContainer._size;
// 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];
@@ -190,11 +228,41 @@ var CPSystemTypesetterFactory,
break;
case CPCenterTextAlignment:
myX = (containerSize.width - _lineWidth) / 2;
myX = (availableWidth - visibleWidth) / 2;
break;
case CPRightTextAlignment:
myX = containerSize.width - _lineWidth;
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;
}
@@ -237,8 +305,10 @@ var CPSystemTypesetterFactory,
_lineBase = 0;
_lineWidth = 0;
// containerSizeWidth is the width inside the line fragment padding at both ends.
var containerSize = [_currentTextContainer containerSize],
containerSizeWidth = containerSize.width,
lineFragmentPadding = [_currentTextContainer lineFragmentPadding],
containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding,
containerSizeHeight = containerSize.height,
lineRange = CPMakeRange(glyphIndex, 0),
wrapRange = CPMakeRange(0, 0),
@@ -274,7 +344,11 @@ var CPSystemTypesetterFactory,
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
@@ -469,7 +543,16 @@ var CPSystemTypesetterFactory,
tabAlignment = [nextTab alignment];
// Mathematically offset the tab character's right boundary
if (tabAlignment === CPCenterTextAlignment)
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;
}
@@ -533,7 +616,7 @@ var CPSystemTypesetterFactory,
if (isNewline || isTabStop || isAttachment)
{
if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline])
if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline wordWrapped:isWordWrapped rightMargin:rightMargin])
return;
if (isTabStop || isAttachment)
@@ -556,7 +639,8 @@ var CPSystemTypesetterFactory,
{
_currentTextContainer = textContainers[++_indexOfCurrentContainer];
containerSize = [_currentTextContainer containerSize];
containerSizeWidth = containerSize.width;
lineFragmentPadding = [_currentTextContainer lineFragmentPadding];
containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding;
containerSizeHeight = containerSize.height;
}
@@ -587,9 +671,10 @@ var CPSystemTypesetterFactory,
// Flush remaining characters
if (lineRange.length)
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO];
[self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO wordWrapped:NO rightMargin:rightMargin];
var rect = CGRectMake(1, lineOrigin.y - descent, containerSizeWidth, currentFontLineHeight);
// 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];