Merge branch 'master' into update-objc

Conflicts:
	AsyncDisplayKit/ASDisplayNodeExtras.h
	AsyncDisplayKit/Details/ASTextNodeRenderer.h
	AsyncDisplayKit/Details/ASTextNodeShadower.h
This commit is contained in:
Adlai Holler
2015-12-01 16:45:25 -08:00
139 changed files with 4747 additions and 1755 deletions

View File

@@ -0,0 +1,171 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
#import <string>
// From folly:
// This is the Hash128to64 function from Google's cityhash (available
// under the MIT License). We use it to reduce multiple 64 bit hashes
// into a single hash.
inline uint64_t ASHashCombine(const uint64_t upper, const uint64_t lower) {
// Murmur-inspired hashing.
const uint64_t kMul = 0x9ddfea08eb382d69ULL;
uint64_t a = (lower ^ upper) * kMul;
a ^= (a >> 47);
uint64_t b = (upper ^ a) * kMul;
b ^= (b >> 47);
b *= kMul;
return b;
}
#if __LP64__
inline size_t ASHash64ToNative(uint64_t key) {
return key;
}
#else
// Thomas Wang downscaling hash function
inline size_t ASHash64ToNative(uint64_t key) {
key = (~key) + (key << 18);
key = key ^ (key >> 31);
key = key * 21;
key = key ^ (key >> 11);
key = key + (key << 6);
key = key ^ (key >> 22);
return (uint32_t) key;
}
#endif
NSUInteger ASIntegerArrayHash(const NSUInteger *subhashes, NSUInteger count);
namespace AS {
// Default is not an ObjC class
template<typename T, typename V = bool>
struct is_objc_class : std::false_type { };
// Conditionally enable this template specialization on whether T is convertible to id, makes the is_objc_class a true_type
template<typename T>
struct is_objc_class<T, typename std::enable_if<std::is_convertible<T, id>::value, bool>::type> : std::true_type { };
// ASUtils::hash<T>()(value) -> either std::hash<T> if c++ or [o hash] if ObjC object.
template <typename T, typename Enable = void> struct hash;
// For non-objc types, defer to std::hash
template <typename T> struct hash<T, typename std::enable_if<!is_objc_class<T>::value>::type> {
size_t operator ()(const T& a) {
return std::hash<T>()(a);
}
};
// For objc types, call [o hash]
template <typename T> struct hash<T, typename std::enable_if<is_objc_class<T>::value>::type> {
size_t operator ()(id o) {
return [o hash];
}
};
template <typename T, typename Enable = void> struct is_equal;
// For non-objc types use == operator
template <typename T> struct is_equal<T, typename std::enable_if<!is_objc_class<T>::value>::type> {
bool operator ()(const T& a, const T& b) {
return a == b;
}
};
// For objc types, check pointer equality, then use -isEqual:
template <typename T> struct is_equal<T, typename std::enable_if<is_objc_class<T>::value>::type> {
bool operator ()(id a, id b) {
return a == b || [a isEqual:b];
}
};
};
namespace ASTupleOperations
{
// Recursive case (hash up to Index)
template <class Tuple, size_t Index = std::tuple_size<Tuple>::value - 1>
struct _hash_helper
{
static size_t hash(Tuple const& tuple)
{
size_t prev = _hash_helper<Tuple, Index-1>::hash(tuple);
using TypeForIndex = typename std::tuple_element<Index,Tuple>::type;
size_t thisHash = AS::hash<TypeForIndex>()(std::get<Index>(tuple));
return ASHashCombine(prev, thisHash);
}
};
// Base case (hash 0th element)
template <class Tuple>
struct _hash_helper<Tuple, 0>
{
static size_t hash(Tuple const& tuple)
{
using TypeForIndex = typename std::tuple_element<0,Tuple>::type;
return AS::hash<TypeForIndex>()(std::get<0>(tuple));
}
};
// Recursive case (elements equal up to Index)
template <class Tuple, size_t Index = std::tuple_size<Tuple>::value - 1>
struct _eq_helper
{
static bool equal(Tuple const& a, Tuple const& b)
{
bool prev = _eq_helper<Tuple, Index-1>::equal(a, b);
using TypeForIndex = typename std::tuple_element<Index,Tuple>::type;
auto aValue = std::get<Index>(a);
auto bValue = std::get<Index>(b);
return prev && AS::is_equal<TypeForIndex>()(aValue, bValue);
}
};
// Base case (0th elements equal)
template <class Tuple>
struct _eq_helper<Tuple, 0>
{
static bool equal(Tuple const& a, Tuple const& b)
{
using TypeForIndex = typename std::tuple_element<0,Tuple>::type;
auto& aValue = std::get<0>(a);
auto& bValue = std::get<0>(b);
return AS::is_equal<TypeForIndex>()(aValue, bValue);
}
};
template <typename ... TT> struct hash;
template <typename ... TT>
struct hash<std::tuple<TT...>>
{
size_t operator()(std::tuple<TT...> const& tt) const
{
return _hash_helper<std::tuple<TT...>>::hash(tt);
}
};
template <typename ... TT> struct equal_to;
template <typename ... TT>
struct equal_to<std::tuple<TT...>>
{
bool operator()(std::tuple<TT...> const& a, std::tuple<TT...> const& b) const
{
return _eq_helper<std::tuple<TT...>>::equal(a, b);
}
};
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <UIKit/UIKit.h>
#ifndef ComponentKit_ASTextKitAttributes_h
#define ComponentKit_ASTextKitAttributes_h
@protocol ASTextKitTruncating;
extern NSString *const ASTextKitTruncationAttributeName;
/**
Use ASTextKitEntityAttribute as the value of this attribute to embed a link or other interactable content inside the
text.
*/
extern NSString *const ASTextKitEntityAttributeName;
static inline BOOL _objectsEqual(id<NSObject> obj1, id<NSObject> obj2)
{
return obj1 == obj2 ? YES : [obj1 isEqual:obj2];
}
/**
All NSObject values in this struct should be copied when passed into the TextComponent.
*/
struct ASTextKitAttributes {
/**
The string to be drawn. ASTextKit will not augment this string with default colors, etc. so this must be complete.
*/
NSAttributedString *attributedString;
/**
The string to use as the truncation string, usually just "...". If you have a range of text you would like to
restrict highlighting to (for instance if you have "... Continue Reading", use the ASTextKitTruncationAttributeName
to mark the specific range of the string that should be highlightable.
*/
NSAttributedString *truncationAttributedString;
/**
This is the character set that ASTextKit should attempt to avoid leaving as a trailing character before your
truncation token. By default this set includes "\s\t\n\r.,!?:;" so you don't end up with ugly looking truncation
text like "Hey, this is some fancy Truncation!\n\n...". Instead it would be truncated as "Hey, this is some fancy
truncation...". This is not always possible.
Set this to the empty charset if you want to just use the "dumb" truncation behavior. A nil value will be
substituted with the default described above.
*/
NSCharacterSet *avoidTailTruncationSet;
/**
The line-break mode to apply to the text. Since this also impacts how TextKit will attempt to truncate the text
in your string, we only support NSLineBreakByWordWrapping and NSLineBreakByCharWrapping.
*/
NSLineBreakMode lineBreakMode;
/**
The maximum number of lines to draw in the drawable region. Leave blank or set to 0 to define no maximum.
*/
NSUInteger maximumNumberOfLines;
/**
An array of UIBezierPath objects representing the exclusion paths inside the receiver's bounding rectangle. Default value: nil.
*/
NSArray *exclusionPaths;
/**
The shadow offset for any shadows applied to the text. The coordinate space for this is the same as UIKit, so a
positive width means towards the right, and a positive height means towards the bottom.
*/
CGSize shadowOffset;
/**
The color to use in drawing the text's shadow.
*/
UIColor *shadowColor;
/**
The opacity of the shadow from 0 to 1.
*/
CGFloat shadowOpacity;
/**
The radius that should be applied to the shadow blur. Larger values mean a larger, more blurred shadow.
*/
CGFloat shadowRadius;
/**
A pointer to a function that that returns a custom layout manager subclass. If nil, defaults to NSLayoutManager.
*/
NSLayoutManager *(*layoutManagerFactory)(void);
/**
We provide an explicit copy function so we can use aggregate initializer syntax while providing copy semantics for
the NSObjects inside.
*/
const ASTextKitAttributes copy() const
{
return {
[attributedString copy],
[truncationAttributedString copy],
[avoidTailTruncationSet copy],
lineBreakMode,
maximumNumberOfLines,
[exclusionPaths copy],
shadowOffset,
[shadowColor copy],
shadowOpacity,
shadowRadius,
layoutManagerFactory
};
};
bool operator==(const ASTextKitAttributes &other) const
{
// These comparisons are in a specific order to reduce the overall cost of this function.
return lineBreakMode == other.lineBreakMode
&& maximumNumberOfLines == other.maximumNumberOfLines
&& shadowOpacity == other.shadowOpacity
&& shadowRadius == other.shadowRadius
&& layoutManagerFactory == other.layoutManagerFactory
&& CGSizeEqualToSize(shadowOffset, other.shadowOffset)
&& _objectsEqual(exclusionPaths, other.exclusionPaths)
&& _objectsEqual(avoidTailTruncationSet, other.avoidTailTruncationSet)
&& _objectsEqual(shadowColor, other.shadowColor)
&& _objectsEqual(attributedString, other.attributedString)
&& _objectsEqual(truncationAttributedString, other.truncationAttributedString);
}
size_t hash() const;
};
#endif

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASTextKitAttributes.h"
#import "ASEqualityHashHelpers.h"
#include <functional>
NSString *const ASTextKitTruncationAttributeName = @"ck_truncation";
NSString *const ASTextKitEntityAttributeName = @"ck_entity";
size_t ASTextKitAttributes::hash() const
{
NSUInteger subhashes[] = {
[attributedString hash],
[truncationAttributedString hash],
[avoidTailTruncationSet hash],
std::hash<NSUInteger>()((NSUInteger) layoutManagerFactory),
std::hash<NSInteger>()(lineBreakMode),
std::hash<NSInteger>()(maximumNumberOfLines),
[exclusionPaths hash],
std::hash<CGFloat>()(shadowOffset.width),
std::hash<CGFloat>()(shadowOffset.height),
[shadowColor hash],
std::hash<CGFloat>()(shadowOpacity),
std::hash<CGFloat>()(shadowRadius),
};
return ASIntegerArrayHash(subhashes, sizeof(subhashes) / sizeof(subhashes[0]));
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <UIKit/UIKit.h>
/**
A threadsafe container for the TextKit components that ASTextKit uses to lay out and truncate its text.
This container is the sole owner and manager of the TextKit classes. This is an important model because of major
thread safety issues inside vanilla TextKit. It provides a central locking location for accessing TextKit methods.
*/
@interface ASTextKitContext : NSObject
/**
Initializes a context and its associated TextKit components.
Initialization of TextKit components is a globally locking operation so be careful of bottlenecks with this class.
*/
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
lineBreakMode:(NSLineBreakMode)lineBreakMode
maximumNumberOfLines:(NSUInteger)maximumNumberOfLines
exclusionPaths:(NSArray *)exclusionPaths
constrainedSize:(CGSize)constrainedSize
layoutManagerFactory:(NSLayoutManager*(*)(void))layoutManagerFactory;
/**
All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to
TextKit components may cause crashes.
The block provided MUST not call out to client code from within its scope or it is possible for this to cause deadlocks
in your application. Use with EXTREME care.
Callers MUST NOT keep a ref to these internal objects and use them later. This WILL cause crashes in your application.
*/
- (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *layoutManager,
NSTextStorage *textStorage,
NSTextContainer *textContainer))block;
@end

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <mutex>
#import "ASTextKitContext.h"
@implementation ASTextKitContext
{
// All TextKit operations (even non-mutative ones) must be executed serially.
std::mutex _textKitMutex;
NSLayoutManager *_layoutManager;
NSTextStorage *_textStorage;
NSTextContainer *_textContainer;
}
- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
lineBreakMode:(NSLineBreakMode)lineBreakMode
maximumNumberOfLines:(NSUInteger)maximumNumberOfLines
exclusionPaths:(NSArray *)exclusionPaths
constrainedSize:(CGSize)constrainedSize
layoutManagerFactory:(NSLayoutManager*(*)(void))layoutManagerFactory
{
if (self = [super init]) {
// Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock.
static std::mutex __static_mutex;
std::lock_guard<std::mutex> l(__static_mutex);
// Create the TextKit component stack with our default configuration.
_textStorage = (attributedString ? [[NSTextStorage alloc] initWithAttributedString:attributedString] : [[NSTextStorage alloc] init]);
_layoutManager = layoutManagerFactory ? layoutManagerFactory() : [[NSLayoutManager alloc] init];
_layoutManager.usesFontLeading = NO;
[_textStorage addLayoutManager:_layoutManager];
_textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize];
// We want the text laid out up to the very edges of the container.
_textContainer.lineFragmentPadding = 0;
_textContainer.lineBreakMode = lineBreakMode;
_textContainer.maximumNumberOfLines = maximumNumberOfLines;
_textContainer.exclusionPaths = exclusionPaths;
[_layoutManager addTextContainer:_textContainer];
}
return self;
}
- (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *,
NSTextStorage *,
NSTextContainer *))block
{
std::lock_guard<std::mutex> l(_textKitMutex);
block(_layoutManager, _textStorage, _textContainer);
}
@end

View File

@@ -0,0 +1,86 @@
/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
NS_ASSUME_NONNULL_BEGIN
ASDISPLAYNODE_EXTERN_C_BEGIN
/**
@abstract Returns whether a given attribute is an unsupported Core Text attribute.
@param attributeName The name of the attribute
@discussion The following Core Text attributes are not supported on NSAttributedString, and thus will not be preserved during the conversion:
- kCTForegroundColorFromContextAttributeName
- kCTSuperscriptAttributeName
- kCTGlyphInfoAttributeName
- kCTCharacterShapeAttributeName
- kCTLanguageAttributeName
- kCTRunDelegateAttributeName
- kCTBaselineClassAttributeName
- kCTBaselineInfoAttributeName
- kCTBaselineReferenceInfoAttributeName
- kCTWritingDirectionAttributeName
- kCTUnderlineColorAttributeName
@result Whether attributeName is an unsupported Core Text attribute.
*/
BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName);
/**
@abstract Returns an attributes dictionary for use by NSAttributedString, given a dictionary of Core Text attributes.
@param coreTextAttributes An NSDictionary whose keys are CFAttributedStringRef attributes.
@discussion The following Core Text attributes are not supported on NSAttributedString, and thus will not be preserved during the conversion:
- kCTForegroundColorFromContextAttributeName
- kCTSuperscriptAttributeName
- kCTGlyphInfoAttributeName
- kCTCharacterShapeAttributeName
- kCTLanguageAttributeName
- kCTRunDelegateAttributeName
- kCTBaselineClassAttributeName
- kCTBaselineInfoAttributeName
- kCTBaselineReferenceInfoAttributeName
- kCTWritingDirectionAttributeName
- kCTUnderlineColorAttributeName
@result An NSDictionary of attributes for use by NSAttributedString.
*/
extern NSDictionary *NSAttributedStringAttributesForCoreTextAttributes(NSDictionary *coreTextAttributes);
/**
@abstract Returns an NSAttributedString whose Core Text attributes have been converted, where possible, to NSAttributedString attributes.
@param dirtyAttributedString An NSAttributedString that may contain Core Text attributes.
@result An NSAttributedString that's preserved as many CFAttributedString attributes as possible.
*/
extern NSAttributedString *ASCleanseAttributedStringOfCoreTextAttributes(NSAttributedString *dirtyAttributedString);
ASDISPLAYNODE_EXTERN_C_END
#pragma mark -
#pragma mark -
@interface NSParagraphStyle (ASTextKitCoreTextAdditions)
/**
@abstract Returns an NSParagraphStyle initialized with the paragraph specifiers from the given CTParagraphStyleRef.
@param coreTextParagraphStyle A Core Text paragraph style.
@discussion It is important to note that not all CTParagraphStyle specifiers are supported by NSParagraphStyle, and consequently, this is a lossy conversion. Notably, the following specifiers will not preserved:
- kCTParagraphStyleSpecifierTabStops
- kCTParagraphStyleSpecifierDefaultTabInterval
- kCTParagraphStyleSpecifierMaximumLineSpacing
- kCTParagraphStyleSpecifierMinimumLineSpacing
- kCTParagraphStyleSpecifierLineSpacingAdjustment
- kCTParagraphStyleSpecifierLineBoundsOptions
@result An NSParagraphStyle initializd with as many of the paragraph specifiers from `coreTextParagraphStyle` as possible.
*/
+ (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,246 @@
/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "ASTextKitCoreTextAdditions.h"
#import <CoreText/CTFont.h>
#import <CoreText/CTStringAttributes.h>
#import "ASAssert.h"
#pragma mark - Public
BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName)
{
static NSSet *coreTextAttributes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
coreTextAttributes = [NSSet setWithObjects:(__bridge id)kCTForegroundColorAttributeName,
kCTForegroundColorFromContextAttributeName,
kCTForegroundColorAttributeName,
kCTStrokeColorAttributeName,
kCTUnderlineStyleAttributeName,
kCTVerticalFormsAttributeName,
kCTRunDelegateAttributeName,
kCTBaselineClassAttributeName,
kCTBaselineInfoAttributeName,
kCTBaselineReferenceInfoAttributeName,
kCTUnderlineColorAttributeName,
nil];
});
return [coreTextAttributes containsObject:attributeName];
}
NSDictionary *NSAttributedStringAttributesForCoreTextAttributes(NSDictionary *coreTextAttributes)
{
NSMutableDictionary *cleanAttributes = [[NSMutableDictionary alloc] initWithCapacity:coreTextAttributes.count];
[coreTextAttributes enumerateKeysAndObjectsUsingBlock:^(NSString *coreTextKey, id coreTextValue, BOOL *stop) {
// The following attributes are not supported on NSAttributedString. Should they become available, we should add them.
/*
kCTForegroundColorFromContextAttributeName
kCTSuperscriptAttributeName
kCTGlyphInfoAttributeName
kCTCharacterShapeAttributeName
kCTLanguageAttributeName
kCTRunDelegateAttributeName
kCTBaselineClassAttributeName
kCTBaselineInfoAttributeName
kCTBaselineReferenceInfoAttributeName
kCTWritingDirectionAttributeName
kCTUnderlineColorAttributeName
*/
// Conversely, the following attributes are not supported on CFAttributedString. Should they become available, we should add them.
/*
NSStrikethroughStyleAttributeName
NSShadowAttributeName
NSBackgroundColorAttributeName
*/
// kCTFontAttributeName -> NSFontAttributeName
if ([coreTextKey isEqualToString:(NSString *)kCTFontAttributeName]) {
CTFontRef coreTextFont = (__bridge CTFontRef)coreTextValue;
NSString *fontName = (__bridge_transfer NSString *)CTFontCopyPostScriptName(coreTextFont);
CGFloat fontSize = CTFontGetSize(coreTextFont);
UIFont *font = [UIFont fontWithName:fontName size:fontSize];
ASDisplayNodeCAssertNotNil(font, @"unable to load font %@ with size %f", fontName, fontSize);
if (font == nil) {
// Gracefully fail if we were unable to load the font.
font = [UIFont systemFontOfSize:fontSize];
}
cleanAttributes[NSFontAttributeName] = font;
}
// kCTKernAttributeName -> NSKernAttributeName
else if ([coreTextKey isEqualToString:(NSString *)kCTKernAttributeName]) {
cleanAttributes[NSKernAttributeName] = (NSNumber *)coreTextValue;
}
// kCTLigatureAttributeName -> NSLigatureAttributeName
else if ([coreTextKey isEqualToString:(NSString *)kCTLigatureAttributeName]) {
cleanAttributes[NSLigatureAttributeName] = (NSNumber *)coreTextValue;
}
// kCTForegroundColorAttributeName -> NSForegroundColorAttributeName
else if ([coreTextKey isEqualToString:(NSString *)kCTForegroundColorAttributeName]) {
cleanAttributes[NSForegroundColorAttributeName] = [UIColor colorWithCGColor:(CGColorRef)coreTextValue];
}
// kCTParagraphStyleAttributeName -> NSParagraphStyleAttributeName
else if ([coreTextKey isEqualToString:(NSString *)kCTParagraphStyleAttributeName]) {
cleanAttributes[NSParagraphStyleAttributeName] = [NSParagraphStyle paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextValue];
}
// kCTStrokeWidthAttributeName -> NSStrokeWidthAttributeName
else if ([coreTextKey isEqualToString:(NSString *)kCTStrokeWidthAttributeName]) {
cleanAttributes[NSStrokeWidthAttributeName] = (NSNumber *)coreTextValue;
}
// kCTStrokeColorAttributeName -> NSStrokeColorAttributeName
else if ([coreTextKey isEqualToString:(NSString *)kCTStrokeColorAttributeName]) {
cleanAttributes[NSStrokeColorAttributeName] = [UIColor colorWithCGColor:(CGColorRef)coreTextValue];
}
// kCTUnderlineStyleAttributeName -> NSUnderlineStyleAttributeName
else if ([coreTextKey isEqualToString:(NSString *)kCTUnderlineStyleAttributeName]) {
cleanAttributes[NSUnderlineStyleAttributeName] = (NSNumber *)coreTextValue;
}
// kCTVerticalFormsAttributeName -> NSVerticalGlyphFormAttributeName
else if ([coreTextKey isEqualToString:(NSString *)kCTVerticalFormsAttributeName]) {
BOOL flag = (BOOL)CFBooleanGetValue((CFBooleanRef)coreTextValue);
cleanAttributes[NSVerticalGlyphFormAttributeName] = @((int)flag); // NSVerticalGlyphFormAttributeName is documented to be an NSNumber with an integer that's either 0 or 1.
}
// Don't filter out any internal text attributes
else if (!ASAttributeWithNameIsUnsupportedCoreTextAttribute(coreTextKey)){
cleanAttributes[coreTextKey] = coreTextValue;
}
}];
return cleanAttributes;
}
NSAttributedString *ASCleanseAttributedStringOfCoreTextAttributes(NSAttributedString *dirtyAttributedString)
{
if (!dirtyAttributedString)
return nil;
// First see if there are any core text attributes on the string
__block BOOL containsCoreTextAttributes = NO;
[dirtyAttributedString enumerateAttributesInRange:NSMakeRange(0, dirtyAttributedString.length)
options:0
usingBlock:^(NSDictionary *dirtyAttributes, NSRange range, BOOL *stop) {
[dirtyAttributes enumerateKeysAndObjectsUsingBlock:^(NSString *coreTextKey, id coreTextValue, BOOL *innerStop) {
if (ASAttributeWithNameIsUnsupportedCoreTextAttribute(coreTextKey)) {
containsCoreTextAttributes = YES;
*innerStop = YES;
}
}];
*stop = containsCoreTextAttributes;
}];
if (containsCoreTextAttributes) {
NSString *plainString = dirtyAttributedString.string;
NSMutableAttributedString *cleanAttributedString = [[NSMutableAttributedString alloc] initWithString:plainString];
// Iterate over all of the attributes, cleaning them as appropriate and applying them as we go.
[dirtyAttributedString enumerateAttributesInRange:NSMakeRange(0, plainString.length)
options:0
usingBlock:^(NSDictionary *dirtyAttributes, NSRange range, BOOL *stop) {
[cleanAttributedString addAttributes:NSAttributedStringAttributesForCoreTextAttributes(dirtyAttributes) range:range];
}];
return cleanAttributedString;
} else {
return [dirtyAttributedString copy];
}
}
#pragma mark -
#pragma mark -
@implementation NSParagraphStyle (ASTextKitCoreTextAdditions)
+ (instancetype)paragraphStyleWithCTParagraphStyle:(CTParagraphStyleRef)coreTextParagraphStyle;
{
NSMutableParagraphStyle *newParagraphStyle = [[NSMutableParagraphStyle alloc] init];
if (!coreTextParagraphStyle)
return newParagraphStyle;
// The following paragraph style specifiers are not supported on NSParagraphStyle. Should they become available, we should add them.
/*
kCTParagraphStyleSpecifierTabStops
kCTParagraphStyleSpecifierDefaultTabInterval
kCTParagraphStyleSpecifierMaximumLineSpacing
kCTParagraphStyleSpecifierMinimumLineSpacing
kCTParagraphStyleSpecifierLineSpacingAdjustment
kCTParagraphStyleSpecifierLineBoundsOptions
*/
// Conversely, the following paragraph styles are not supported on CTParagraphStyle. Should they become available, we should add them.
/*
hyphenationFactor
*/
// kCTParagraphStyleSpecifierAlignment -> alignment
CTTextAlignment coreTextAlignment;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierAlignment, sizeof(coreTextAlignment), &coreTextAlignment))
newParagraphStyle.alignment = NSTextAlignmentFromCTTextAlignment(coreTextAlignment);
// kCTParagraphStyleSpecifierFirstLineHeadIndent -> firstLineHeadIndent
CGFloat firstLineHeadIndent;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(firstLineHeadIndent), &firstLineHeadIndent))
newParagraphStyle.firstLineHeadIndent = firstLineHeadIndent;
// kCTParagraphStyleSpecifierHeadIndent -> headIndent
CGFloat headIndent;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierHeadIndent, sizeof(headIndent), &headIndent))
newParagraphStyle.headIndent = headIndent;
// kCTParagraphStyleSpecifierTailIndent -> tailIndent
CGFloat tailIndent;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierTailIndent, sizeof(tailIndent), &tailIndent))
newParagraphStyle.tailIndent = tailIndent;
// kCTParagraphStyleSpecifierLineBreakMode -> lineBreakMode
CTLineBreakMode coreTextLineBreakMode;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineBreakMode, sizeof(coreTextLineBreakMode), &coreTextLineBreakMode))
newParagraphStyle.lineBreakMode = (NSLineBreakMode)coreTextLineBreakMode; // They're the same enum.
// kCTParagraphStyleSpecifierLineHeightMultiple -> lineHeightMultiple
CGFloat lineHeightMultiple;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineHeightMultiple, sizeof(lineHeightMultiple), &lineHeightMultiple))
newParagraphStyle.lineHeightMultiple = lineHeightMultiple;
// kCTParagraphStyleSpecifierMaximumLineHeight -> maximumLineHeight
CGFloat maximumLineHeight;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(maximumLineHeight), &maximumLineHeight))
newParagraphStyle.maximumLineHeight = maximumLineHeight;
// kCTParagraphStyleSpecifierMinimumLineHeight -> minimumLineHeight
CGFloat minimumLineHeight;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(minimumLineHeight), &minimumLineHeight))
newParagraphStyle.minimumLineHeight = minimumLineHeight;
// kCTParagraphStyleSpecifierLineSpacing -> lineSpacing
// Note that kCTParagraphStyleSpecifierLineSpacing is deprecated and will die soon. We should not be using it.
CGFloat lineSpacing;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierLineSpacing, sizeof(lineSpacing), &lineSpacing))
newParagraphStyle.lineSpacing = lineSpacing;
// kCTParagraphStyleSpecifierParagraphSpacing -> paragraphSpacing
CGFloat paragraphSpacing;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacing, sizeof(paragraphSpacing), &paragraphSpacing))
newParagraphStyle.paragraphSpacing = paragraphSpacing;
// kCTParagraphStyleSpecifierParagraphSpacingBefore -> paragraphSpacingBefore
CGFloat paragraphSpacingBefore;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(paragraphSpacingBefore), &paragraphSpacingBefore))
newParagraphStyle.paragraphSpacingBefore = paragraphSpacingBefore;
// kCTParagraphStyleSpecifierBaseWritingDirection -> baseWritingDirection
CTWritingDirection coreTextBaseWritingDirection;
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(coreTextBaseWritingDirection), &coreTextBaseWritingDirection))
newParagraphStyle.baseWritingDirection = (NSWritingDirection)coreTextBaseWritingDirection; // They're the same enum.
return newParagraphStyle;
}
@end

View File

@@ -0,0 +1,28 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
/**
The object that should be embedded with ASTextKitEntityAttributeName. Please note that the entity you provide MUST
implement a proper hash and isEqual function or your application performance will grind to a halt due to
NSMutableAttributedString's usage of a global hash table of all attributes. This means the entity should NOT be a
Foundation Collection (NSArray, NSDictionary, NSSet, etc.) since their hash function is a simple count of the values
in the collection, which causes pathological performance problems deep inside NSAttributedString's implementation.
rdar://19352367
*/
@interface ASTextKitEntityAttribute : NSObject
@property (nonatomic, strong, readonly) id<NSObject> entity;
- (instancetype)initWithEntity:(id<NSObject>)entity;
@end

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASTextKitEntityAttribute.h"
@implementation ASTextKitEntityAttribute
- (instancetype)initWithEntity:(id<NSObject>)entity
{
if (self = [super init]) {
_entity = entity;
}
return self;
}
- (NSUInteger)hash
{
return [_entity hash];
}
- (BOOL)isEqual:(id)object
{
if (self == object) {
return YES;
}
if (![object isKindOfClass:[self class]]) {
return NO;
}
ASTextKitEntityAttribute *other = (ASTextKitEntityAttribute *)object;
return _entity == other.entity || [_entity isEqual:other.entity];
}
@end

View File

@@ -0,0 +1,58 @@
/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <objc/message.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "ASBaseDefines.h"
NS_ASSUME_NONNULL_BEGIN
ASDISPLAYNODE_INLINE CGFloat ceilPixelValueForScale(CGFloat f, CGFloat scale)
{
// Round up to device pixel (.5 on retina)
return ceilf(f * scale) / scale;
}
ASDISPLAYNODE_INLINE CGSize ceilSizeValue(CGSize s)
{
CGFloat screenScale = [UIScreen mainScreen].scale;
s.width = ceilPixelValueForScale(s.width, screenScale);
s.height = ceilPixelValueForScale(s.height, screenScale);
return s;
}
@interface ASTextKitComponents : NSObject
/**
@abstract Creates the stack of TextKit components.
@param attributedSeedString The attributed string to seed the returned text storage with, or nil to receive an blank text storage.
@param textContainerSize The size of the text-container. Typically, size specifies the constraining width of the layout, and FLT_MAX for height. Pass CGSizeZero if these components will be hooked up to a UITextView, which will manage the text container's size itself.
@return An `ASTextKitComponents` containing the created components. The text view component will be nil.
@discussion The returned components will be hooked up together, so they are ready for use as a system upon return.
*/
+ (ASTextKitComponents *)componentsWithAttributedSeedString:(nullable NSAttributedString *)attributedSeedString
textContainerSize:(CGSize)textContainerSize;
/**
@abstract Returns the bounding size for the text view's text.
@param components The TextKit components to calculate the constrained size of the text for.
@param constrainedWidth The constraining width to be used during text-sizing. Usually, this value should be the receiver's calculated size.
@result A CGSize representing the bounding size for the receiver's text.
*/
- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth;
@property (nonatomic, strong, readonly) NSTextStorage *textStorage;
@property (nonatomic, strong, readonly) NSTextContainer *textContainer;
@property (nonatomic, strong, readonly) NSLayoutManager *layoutManager;
@property (nullable, nonatomic, strong) UITextView *textView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,57 @@
/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "ASTextKitHelpers.h"
@interface ASTextKitComponents ()
// read-write redeclarations
@property (nonatomic, strong, readwrite) NSTextStorage *textStorage;
@property (nonatomic, strong, readwrite) NSTextContainer *textContainer;
@property (nonatomic, strong, readwrite) NSLayoutManager *layoutManager;
@end
@implementation ASTextKitComponents
+ (ASTextKitComponents *)componentsWithAttributedSeedString:(NSAttributedString *)attributedSeedString
textContainerSize:(CGSize)textContainerSize
{
ASTextKitComponents *components = [[ASTextKitComponents alloc] init];
// Create the TextKit component stack with our default configuration.
components.textStorage = (attributedSeedString ? [[NSTextStorage alloc] initWithAttributedString:attributedSeedString] : [[NSTextStorage alloc] init]);
components.layoutManager = [[NSLayoutManager alloc] init];
[components.textStorage addLayoutManager:components.layoutManager];
components.textContainer = [[NSTextContainer alloc] initWithSize:textContainerSize];
components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view.
[components.layoutManager addTextContainer:components.textContainer];
return components;
}
- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth
{
ASTextKitComponents *components = self;
// If our text-view's width is already the constrained width, we can use our existing TextKit stack for this sizing calculation.
// Otherwise, we create a temporary stack to size for `constrainedWidth`.
if (CGRectGetWidth(components.textView.bounds) != constrainedWidth) {
components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, FLT_MAX)];
}
// Force glyph generation and layout, which may not have happened yet (and isn't triggered by -usedRectForTextContainer:).
[components.layoutManager ensureLayoutForTextContainer:components.textContainer];
CGSize textSize = [components.layoutManager usedRectForTextContainer:components.textContainer].size;
return textSize;
}
@end

View File

@@ -0,0 +1,103 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASTextKitRenderer.h"
typedef void (^as_text_component_index_block_t)(NSUInteger characterIndex,
CGRect glyphBoundingRect,
BOOL *stop);
/**
Measure options are used to specify which type of line height measurement to use.
ASTextNodeRendererMeasureOptionLineHeight is faster and will give the height from the baseline to the next line.
ASTextNodeRendererMeasureOptionCapHeight is a more nuanced measure of the glyphs in the given range that attempts to
produce a visually balanced rectangle above and below the glyphs to produce nice looking text highlights.
ASTextNodeRendererMeasureOptionBlock uses the cap height option to generate each glyph index, but combines all but the
first and last line rect into a single block. Looks nice for multiline selection.
*/
typedef NS_ENUM(NSUInteger, ASTextKitRendererMeasureOption) {
ASTextKitRendererMeasureOptionLineHeight,
ASTextKitRendererMeasureOptionCapHeight,
ASTextKitRendererMeasureOptionBlock
};
@interface ASTextKitRenderer (Positioning)
/**
Returns the bounding rect for the given character range.
@param textRange The character range for which the bounding rect will be computed. Should be within the range of the
attributedString of this renderer.
@discussion In the external, shadowed coordinate space.
*/
- (CGRect)frameForTextRange:(NSRange)textRange;
/**
Returns an array of rects representing the lines in the given character range
@param textRange The character range for which the rects will be computed. Should be within the range of the
attributedString of this renderer.
@param measureOption The measure option to use for construction of the rects. See ASTextKitRendererMeasureOption
docs for usage.
@discussion This method is useful for providing highlighting text. Returned rects are in the coordinate space of the
renderer.
Triggers initialization of textkit components, truncation, and sizing.
*/
- (NSArray *)rectsForTextRange:(NSRange)textRange
measureOption:(ASTextKitRendererMeasureOption)measureOption;
/**
Enumerate the text character indexes at a position within the coordinate space of the renderer.
@param position The point in the shadowed coordinate space at which text indexes will be enumerated.
@param block The block that will be executed for each index identified that may correspond to the given position. The
block is given the character index that corresponds to the glyph at each index in question, as well as the bounding
rect for that glyph.
@discussion Glyph location based on a touch point is not an exact science because user touches are not well-represented
by a simple point, especially in the context of link-heavy text. So we have this method to make it a bit easier. This
method checks a grid of candidate positions around the touch point you give it, and computes the bounding rect of the
glyph corresponding to the character index given.
The bounding rect of the glyph can be used to identify the best glyph index that corresponds to your touch. For
instance, comparing centroidal distance from the glyph bounding rect to the touch center is useful for identifying
which link a user actually intended to select.
Triggers initialization of textkit components, truncation, and sizing.
*/
- (void)enumerateTextIndexesAtPosition:(CGPoint)position
usingBlock:(as_text_component_index_block_t)block;
/**
Returns the single text index whose glyph's centroid is closest to the given position.
@param position The point in the shadowed coordinate space that should be checked.
@discussion This will use the grid enumeration function above, `enumerateTextIndexesAtPosition...`, in order to find
the closest glyph, so it is possible that a glyph could be missed, but ultimately unlikely.
*/
- (NSUInteger)nearestTextIndexAtPosition:(CGPoint)position;
/**
Returns the trailing rect unused by the renderer in the last rendered line.
@discussion In the external shadowed coordinate space.
Triggers initialization of textkit components, truncation, and sizing.
*/
- (CGRect)trailingRect;
@end

View File

@@ -0,0 +1,374 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASTextKitRenderer+Positioning.h"
#import <CoreText/CoreText.h>
#import "ASAssert.h"
#import "ASTextKitContext.h"
#import "ASTextKitShadower.h"
static const CGFloat ASTextKitRendererGlyphTouchHitSlop = 5.0;
static const CGFloat ASTextKitRendererTextCapHeightPadding = 1.3;
@implementation ASTextKitRenderer (Tracking)
- (NSArray *)rectsForTextRange:(NSRange)textRange
measureOption:(ASTextKitRendererMeasureOption)measureOption
{
__block NSArray *textRects = @[];
[self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
BOOL textRangeIsValid = (NSMaxRange(textRange) <= [textStorage length]);
ASDisplayNodeCAssertTrue(textRangeIsValid);
if (!textRangeIsValid) {
return;
}
// Used for block measure option
__block CGRect firstRect = CGRectNull;
__block CGRect lastRect = CGRectNull;
__block CGRect blockRect = CGRectNull;
NSMutableArray *mutableTextRects = [NSMutableArray array];
NSString *string = textStorage.string;
NSRange totalGlyphRange = [layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL];
[layoutManager enumerateLineFragmentsForGlyphRange:totalGlyphRange usingBlock:^(CGRect rect,
CGRect usedRect,
NSTextContainer *innerTextContainer,
NSRange glyphRange,
BOOL *stop) {
CGRect lineRect = CGRectNull;
// If we're empty, don't bother looping through glyphs, use the default.
if (CGRectIsEmpty(usedRect)) {
lineRect = usedRect;
} else {
// TextKit's bounding rect computations are just a touch off, so we actually
// compose the rects by hand from the center of the given TextKit bounds and
// imposing the font attributes returned by the glyph's font.
NSRange lineGlyphRange = NSIntersectionRange(totalGlyphRange, glyphRange);
for (NSUInteger i = lineGlyphRange.location; i < NSMaxRange(lineGlyphRange) && i < string.length; i++) {
// We grab the properly sized rect for the glyph
CGRect properGlyphRect = [self _internalRectForGlyphAtIndex:i
measureOption:measureOption
layoutManager:layoutManager
textContainer:textContainer
textStorage:textStorage];
// Don't count empty glyphs towards our line rect.
if (!CGRectIsEmpty(properGlyphRect)) {
lineRect = CGRectIsNull(lineRect) ? properGlyphRect
: CGRectUnion(lineRect, properGlyphRect);
}
}
}
if (!CGRectIsNull(lineRect)) {
if (measureOption == ASTextKitRendererMeasureOptionBlock) {
// For the block measurement option we store the first & last rect as
// special cases, then merge everything else into a single block rect
if (CGRectIsNull(firstRect)) {
// We don't have a firstRect, so we must be on the first line.
firstRect = lineRect;
} else if(CGRectIsNull(lastRect)) {
// We don't have a lastRect, but we do have a firstRect, so we must
// be on the second line. No need to merge in the blockRect just yet
lastRect = lineRect;
} else if(CGRectIsNull(blockRect)) {
// We have both a first and last rect, so we must be on the third line
// we don't have any blockRect to merge it into, so we just set it
// directly.
blockRect = lastRect;
lastRect = lineRect;
} else {
// Everything is already set, so we just merge this line into the
// block.
blockRect = CGRectUnion(blockRect, lastRect);
lastRect = lineRect;
}
} else {
// If the block option isn't being used then each line is being treated
// individually.
[mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:lineRect]]];
}
}
}];
if (measureOption == ASTextKitRendererMeasureOptionBlock) {
// Block measure option is handled differently with just 3 vars for the entire range.
if (!CGRectIsNull(firstRect)) {
if (!CGRectIsNull(blockRect)) {
CGFloat rightEdge = MAX(CGRectGetMaxX(blockRect), CGRectGetMaxX(lastRect));
if (rightEdge > CGRectGetMaxX(firstRect)) {
// Force the right side of the first rect to properly align with the
// right side of the rightmost of the block and last rect
firstRect.size.width += rightEdge - CGRectGetMaxX(firstRect);
}
// Force the left side of the block rect to properly align with the
// left side of the leftmost of the first and last rect
blockRect.origin.x = MIN(CGRectGetMinX(firstRect), CGRectGetMinX(lastRect));
// Force the right side of the block rect to properly align with the
// right side of the rightmost of the first and last rect
blockRect.size.width += MAX(CGRectGetMaxX(firstRect), CGRectGetMaxX(lastRect)) - CGRectGetMaxX(blockRect);
}
if (!CGRectIsNull(lastRect)) {
// Force the left edge of the last rect to properly align with the
// left side of the leftmost of the first and block rect, if necessary.
CGFloat leftEdge = MIN(CGRectGetMinX(blockRect), CGRectGetMinX(firstRect));
CGFloat lastRectNudgeAmount = MAX(CGRectGetMinX(lastRect) - leftEdge, 0);
lastRect.origin.x = MIN(leftEdge, CGRectGetMinX(lastRect));
lastRect.size.width += lastRectNudgeAmount;
}
[mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:firstRect]]];
}
if (!CGRectIsNull(blockRect)) {
[mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:blockRect]]];
}
if (!CGRectIsNull(lastRect)) {
[mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:lastRect]]];
}
}
textRects = mutableTextRects;
}];
return textRects;
}
- (NSUInteger)nearestTextIndexAtPosition:(CGPoint)position
{
// Check in a 9-point region around the actual touch point so we make sure
// we get the best attribute for the touch.
__block CGFloat minimumGlyphDistance = CGFLOAT_MAX;
__block NSUInteger minimumGlyphCharacterIndex = NSNotFound;
[self enumerateTextIndexesAtPosition:position usingBlock:^(NSUInteger characterIndex, CGRect glyphBoundingRect, BOOL *stop) {
CGPoint glyphLocation = CGPointMake(CGRectGetMidX(glyphBoundingRect), CGRectGetMidY(glyphBoundingRect));
CGFloat currentDistance = sqrtf(powf(position.x - glyphLocation.x, 2.f) + powf(position.y - glyphLocation.y, 2.f));
if (currentDistance < minimumGlyphDistance) {
minimumGlyphDistance = currentDistance;
minimumGlyphCharacterIndex = characterIndex;
}
}];
return minimumGlyphCharacterIndex;
}
/**
Measured from the internal coordinate space of the context, not accounting for shadow offsets. Actually uses CoreText
as an approximation to work around problems in TextKit's glyph sizing.
*/
- (CGRect)_internalRectForGlyphAtIndex:(NSUInteger)glyphIndex
measureOption:(ASTextKitRendererMeasureOption)measureOption
layoutManager:(NSLayoutManager *)layoutManager
textContainer:(NSTextContainer *)textContainer
textStorage:(NSTextStorage *)textStorage
{
NSUInteger charIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
CGGlyph glyph = [layoutManager glyphAtIndex:glyphIndex];
CTFontRef font = (__bridge_retained CTFontRef)[textStorage attribute:NSFontAttributeName
atIndex:charIndex
effectiveRange:NULL];
if (font == nil) {
font = (__bridge_retained CTFontRef)[UIFont systemFontOfSize:12.0];
}
// Glyph Advance
// +-------------------------+
// | |
// | |
// +------------------------+--|-------------------------|--+-----------+-----+ What TextKit returns sometimes
// | | | XXXXXXXXXXX + | | | (approx. correct height, but
// | ---------|--+---------+ XXX XXXX +|-----------|-----| sometimes inaccurate bounding
// | | | XXX XXXXX| | | widths)
// | | | XX XX | | |
// | | | XX | | |
// | | | XXX | | |
// | | | XX | | |
// | | | XXXXXXXXXXX | | |
// | Cap Height->| | XX | | |
// | | | XX | Ascent-->| |
// | | | XX | | |
// | | | XX | | |
// | | | X | | |
// | | | X | | |
// | | | X | | |
// | | | XX | | |
// | | | X | | |
// | ---------|-------+ X +-------------------------------------|
// | | XX | |
// | | X | |
// | | XX Descent------>| |
// | | XXXXXX | |
// | | XXX | |
// +------------------------+-------------------------------------------------+
// |
// +--+Actual bounding box
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1)
inTextContainer:textContainer];
// If it is a NSTextAttachment, we don't have the matched glyph and use width of glyphRect instead of advance.
CGFloat advance = (glyph == kCGFontIndexInvalid) ? glyphRect.size.width : CTFontGetAdvancesForGlyphs(font, kCTFontOrientationHorizontal, &glyph, NULL, 1);
// We treat the center of the glyph's bounding box as the center of our new rect
CGPoint glyphCenter = CGPointMake(CGRectGetMidX(glyphRect), CGRectGetMidY(glyphRect));
CGRect properGlyphRect;
if (measureOption == ASTextKitRendererMeasureOptionCapHeight
|| measureOption == ASTextKitRendererMeasureOptionBlock) {
CGFloat ascent = CTFontGetAscent(font);
CGFloat descent = CTFontGetDescent(font);
CGFloat capHeight = CTFontGetCapHeight(font);
CGFloat leading = CTFontGetLeading(font);
CGFloat glyphHeight = ascent + descent;
// For visual balance, we add the cap height padding above the cap, and
// below the baseline, we scale by the descent so it grows with the size of
// the text.
CGFloat topPadding = ASTextKitRendererTextCapHeightPadding * descent;
CGFloat bottomPadding = topPadding;
properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5,
glyphCenter.y - glyphHeight * 0.5 + (ascent - capHeight) - topPadding + leading,
advance,
capHeight + topPadding + bottomPadding);
} else {
// We are just measuring the line heights here, so we can use the
// heights used by TextKit, which tend to be pretty good.
properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5,
glyphRect.origin.y,
advance,
glyphRect.size.height);
}
CFRelease(font);
return properGlyphRect;
}
- (void)enumerateTextIndexesAtPosition:(CGPoint)externalPosition usingBlock:(as_text_component_index_block_t)block
{
// This method is a little complex because it has to call out to client code from inside an enumeration that needs
// to achieve a lock on the textkit components. It cannot call out to client code from within that lock so we just
// perform the textkit-locked ops inside the locked context.
ASTextKitContext *lockingContext = self.context;
CGPoint internalPosition = [self.shadower offsetPointWithExternalPoint:externalPosition];
__block BOOL invalidPosition = NO;
[lockingContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
invalidPosition = internalPosition.x > textContainer.size.width
|| internalPosition.y > textContainer.size.height
|| block == NULL;
}];
if (invalidPosition) {
// Short circuit if the position is outside the size of this renderer, or if the block is null.
return;
}
// We break it up into a 44pt box for the touch, and find the closest link attribute-containing glyph to the center of
// the touch.
CGFloat squareSide = 44.f;
// Should be odd if you want to test the center of the touch.
NSInteger pointsOnASide = 3;
// The distance between any 2 of the adjacent points
CGFloat pointSeparation = squareSide / pointsOnASide;
// These are for tracking which point we're on. We start with -pointsOnASide/2 and go to pointsOnASide/2. So if
// pointsOnASide=3, we go from -1 to 1.
NSInteger endIndex = pointsOnASide / 2;
NSInteger startIndex = -endIndex;
BOOL stop = NO;
for (NSInteger i = startIndex; i <= endIndex && !stop; i++) {
for (NSInteger j = startIndex; j <= endIndex && !stop; j++) {
CGPoint currentPoint = CGPointMake(internalPosition.x + i * pointSeparation,
internalPosition.y + j * pointSeparation);
__block NSUInteger characterIndex = NSNotFound;
__block BOOL isValidGlyph = NO;
__block CGRect glyphRect = CGRectNull;
[lockingContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
// We ask the layout manager for the proper glyph at the touch point
NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:currentPoint
inTextContainer:textContainer];
// If it's an invalid glyph, quit.
[layoutManager glyphAtIndex:glyphIndex isValidIndex:&isValidGlyph];
if (!isValidGlyph) {
return;
}
characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
glyphRect = [self _internalRectForGlyphAtIndex:glyphIndex
measureOption:ASTextKitRendererMeasureOptionLineHeight
layoutManager:layoutManager
textContainer:textContainer
textStorage:textStorage];
}];
// Sometimes TextKit plays jokes on us and returns glyphs that really aren't close to the point in question.
// Silly TextKit...
if (!isValidGlyph || !CGRectContainsPoint(CGRectInset(glyphRect, -ASTextKitRendererGlyphTouchHitSlop, -ASTextKitRendererGlyphTouchHitSlop), currentPoint)) {
continue;
}
block(characterIndex, [self.shadower offsetRectWithInternalRect:glyphRect], &stop);
}
}
}
- (CGRect)trailingRect
{
__block CGRect trailingRect = CGRectNull;
[self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
CGSize calculatedSize = textContainer.size;
// If have an empty string, then our whole bounds constitute trailing space.
if ([textStorage length] == 0) {
trailingRect = CGRectMake(0, 0, calculatedSize.width, calculatedSize.height);
return;
}
// Take everything after our final character as trailing space.
NSArray *finalRects = [self rectsForTextRange:NSMakeRange([textStorage length] - 1, 1) measureOption:ASTextKitRendererMeasureOptionLineHeight];
CGRect finalGlyphRect = [[finalRects lastObject] CGRectValue];
CGPoint origin = CGPointMake(CGRectGetMaxX(finalGlyphRect), CGRectGetMinY(finalGlyphRect));
CGSize size = CGSizeMake(calculatedSize.width - origin.x, calculatedSize.height - origin.y);
trailingRect = (CGRect){origin, size};
}];
return trailingRect;
}
- (CGRect)frameForTextRange:(NSRange)textRange
{
__block CGRect textRect = CGRectNull;
[self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
// Bail on invalid range.
if (NSMaxRange(textRange) > [textStorage length]) {
ASDisplayNodeCFailAssert(@"Invalid range");
return;
}
// Force glyph generation and layout.
[layoutManager ensureLayoutForTextContainer:textContainer];
NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL];
textRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
}];
return textRect;
}
@end

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASTextKitRenderer.h"
/**
Application extensions to NSTextCheckingType. We're allowed to do this (see NSTextCheckingAllCustomTypes).
*/
static uint64_t const ASTextKitTextCheckingTypeEntity = 1ULL << 33;
static uint64_t const ASTextKitTextCheckingTypeTruncation = 1ULL << 34;
@class ASTextKitEntityAttribute;
@interface ASTextKitTextCheckingResult : NSTextCheckingResult
@property (nonatomic, strong, readonly) ASTextKitEntityAttribute *entityAttribute;
@end
@interface ASTextKitRenderer (TextChecking)
- (NSTextCheckingResult *)textCheckingResultAtPoint:(CGPoint)point;
@end

View File

@@ -0,0 +1,102 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASTextKitRenderer+TextChecking.h"
#import "ASTextKitAttributes.h"
#import "ASTextKitEntityAttribute.h"
#import "ASTextKitRenderer+Positioning.h"
#import "ASTextKitTailTruncater.h"
@implementation ASTextKitTextCheckingResult
{
// Be explicit about the fact that we are overriding the super class' implementation of -range and -resultType
// and substituting our own custom values. (We could use @synthesize to make these ivars, but our linter correctly
// complains; it's weird to use @synthesize for properties that are redeclared on top of an original declaration in
// the superclass. We only do it here because NSTextCheckingResult doesn't expose an initializer, which is silly.)
NSRange _rangeOverride;
NSTextCheckingType _resultTypeOverride;
}
- (instancetype)initWithType:(NSTextCheckingType)type
entityAttribute:(ASTextKitEntityAttribute *)entityAttribute
range:(NSRange)range
{
if ((self = [super init])) {
_resultTypeOverride = type;
_rangeOverride = range;
_entityAttribute = entityAttribute;
}
return self;
}
- (NSTextCheckingType)resultType
{
return _resultTypeOverride;
}
- (NSRange)range
{
return _rangeOverride;
}
@end
@implementation ASTextKitRenderer (TextChecking)
- (NSTextCheckingResult *)textCheckingResultAtPoint:(CGPoint)point
{
__block NSTextCheckingResult *result = nil;
NSAttributedString *attributedString = self.attributes.attributedString;
NSAttributedString *truncationAttributedString = self.attributes.truncationAttributedString;
// get the index of the last character, so we can handle text in the truncation token
NSRange visibleRange = self.truncater.visibleRanges[0];
__block NSRange truncationTokenRange = { NSNotFound, 0 };
[truncationAttributedString enumerateAttribute:ASTextKitTruncationAttributeName inRange:NSMakeRange(0, truncationAttributedString.length)
options:0
usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value != nil && range.length > 0) {
truncationTokenRange = range;
}
}];
if (truncationTokenRange.location == NSNotFound) {
// The truncation string didn't specify a substring which should be highlighted, so we just highlight it all
truncationTokenRange = { 0, self.attributes.truncationAttributedString.length };
}
truncationTokenRange.location += NSMaxRange(visibleRange);
[self enumerateTextIndexesAtPosition:point usingBlock:^(NSUInteger index, CGRect glyphBoundingRect, BOOL *stop){
if (index >= truncationTokenRange.location) {
result = [[ASTextKitTextCheckingResult alloc] initWithType:ASTextKitTextCheckingTypeTruncation
entityAttribute:nil
range:truncationTokenRange];
} else {
NSRange range;
NSDictionary *attributes = [attributedString attributesAtIndex:index effectiveRange:&range];
ASTextKitEntityAttribute *entityAttribute = attributes[ASTextKitEntityAttributeName];
if (entityAttribute) {
result = [[ASTextKitTextCheckingResult alloc] initWithType:ASTextKitTextCheckingTypeEntity
entityAttribute:entityAttribute
range:range];
}
}
if (result != nil) {
*stop = YES;
}
}];
return result;
}
@end

View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <vector>
#import <UIKit/UIKit.h>
#import "ASTextKitAttributes.h"
@class ASTextKitContext;
@class ASTextKitShadower;
@protocol ASTextKitTruncating;
/**
ASTextKitRenderer is a modular object that is responsible for laying out and drawing text.
A renderer will hold onto the TextKit layouts for the given attributes after initialization. This may constitute a
large amount of memory for large enough applications, so care must be taken when keeping many of these around in-memory
at once.
This object is designed to be modular and simple. All complex maintenance of state should occur in sub-objects or be
derived via pure functions or categories. No touch-related handling belongs in this class.
ALL sizing and layout information from this class is in the external coordinate space of the TextKit components. This
is an important distinction because all internal sizing and layout operations are carried out within the shadowed
coordinate space. Padding will be added for you in order to ensure clipping does not occur, and additional information
on this transform is available via the shadower should you need it.
*/
@interface ASTextKitRenderer : NSObject
/**
Designated Initializer
dvlkferufedgjnhjjfhldjedlunvtdtv
@discussion Sizing will occur as a result of initialization, so be careful when/where you use this.
*/
- (instancetype)initWithTextKitAttributes:(const ASTextKitAttributes &)textComponentAttributes
constrainedSize:(const CGSize)constrainedSize;
@property (nonatomic, strong, readonly) ASTextKitContext *context;
@property (nonatomic, strong, readonly) id<ASTextKitTruncating> truncater;
@property (nonatomic, strong, readonly) ASTextKitShadower *shadower;
@property (nonatomic, assign, readonly) ASTextKitAttributes attributes;
@property (nonatomic, assign, readonly) CGSize constrainedSize;
#pragma mark - Drawing
/*
Draw the renderer's text content into the bounds provided.
@param bounds The rect in which to draw the contents of the renderer.
*/
- (void)drawInContext:(CGContextRef)context bounds:(CGRect)bounds;
#pragma mark - Layout
/*
Returns the computed size of the renderer given the constrained size and other parameters in the initializer.
*/
- (CGSize)size;
#pragma mark - Text Ranges
/*
The character range from the original attributedString that is displayed by the renderer given the parameters in the
initializer.
*/
- (std::vector<NSRange>)visibleRanges;
/*
The number of lines shown in the string.
*/
- (NSUInteger)lineCount;
@end

View File

@@ -0,0 +1,141 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASTextKitRenderer.h"
#import "ASAssert.h"
#import "ASTextKitContext.h"
#import "ASTextKitShadower.h"
#import "ASTextKitTailTruncater.h"
#import "ASTextKitTruncating.h"
static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
{
static NSCharacterSet *truncationCharacterSet;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSMutableCharacterSet *mutableCharacterSet = [[NSMutableCharacterSet alloc] init];
[mutableCharacterSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[mutableCharacterSet addCharactersInString:@".,!?:;"];
truncationCharacterSet = mutableCharacterSet;
});
return truncationCharacterSet;
}
@implementation ASTextKitRenderer {
CGSize _calculatedSize;
}
#pragma mark - Initialization
- (instancetype)initWithTextKitAttributes:(const ASTextKitAttributes &)attributes
constrainedSize:(const CGSize)constrainedSize
{
if (self = [super init]) {
_constrainedSize = constrainedSize;
_attributes = attributes;
_shadower = [[ASTextKitShadower alloc] initWithShadowOffset:attributes.shadowOffset
shadowColor:attributes.shadowColor
shadowOpacity:attributes.shadowOpacity
shadowRadius:attributes.shadowRadius];
// We must inset the constrained size by the size of the shadower.
CGSize shadowConstrainedSize = [_shadower insetSizeWithConstrainedSize:_constrainedSize];
_context = [[ASTextKitContext alloc] initWithAttributedString:attributes.attributedString
lineBreakMode:attributes.lineBreakMode
maximumNumberOfLines:attributes.maximumNumberOfLines
exclusionPaths:attributes.exclusionPaths
constrainedSize:shadowConstrainedSize
layoutManagerFactory:attributes.layoutManagerFactory];
_truncater = [[ASTextKitTailTruncater alloc] initWithContext:_context
truncationAttributedString:attributes.truncationAttributedString
avoidTailTruncationSet:attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet()
constrainedSize:shadowConstrainedSize];
[self _calculateSize];
}
return self;
}
#pragma mark - Sizing
- (void)_calculateSize
{
// Force glyph generation and layout, which may not have happened yet (and isn't triggered by
// -usedRectForTextContainer:).
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
[layoutManager ensureLayoutForTextContainer:textContainer];
}];
CGRect constrainedRect = {CGPointZero, _constrainedSize};
__block CGRect boundingRect;
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
boundingRect = [layoutManager usedRectForTextContainer:textContainer];
}];
// TextKit often returns incorrect glyph bounding rects in the horizontal direction, so we clip to our bounding rect
// to make sure our width calculations aren't being offset by glyphs going beyond the constrained rect.
boundingRect = CGRectIntersection(boundingRect, {.size = constrainedRect.size});
_calculatedSize = [_shadower outsetSizeWithInsetSize:boundingRect.size];
}
- (CGSize)size
{
return _calculatedSize;
}
#pragma mark - Drawing
- (void)drawInContext:(CGContextRef)context bounds:(CGRect)bounds;
{
// We add an assertion so we can track the rare conditions where a graphics context is not present
ASDisplayNodeAssertNotNil(context, @"This is no good without a context.");
CGRect shadowInsetBounds = [_shadower insetRectWithConstrainedRect:bounds];
CGContextSaveGState(context);
[_shadower setShadowInContext:context];
UIGraphicsPushContext(context);
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin];
}];
UIGraphicsPopContext();
CGContextRestoreGState(context);
}
#pragma mark - String Ranges
- (NSUInteger)lineCount
{
__block NSUInteger lineCount = 0;
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [layoutManager numberOfGlyphs]; lineCount++) {
[layoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange];
}
}];
return lineCount;
}
- (std::vector<NSRange>)visibleRanges
{
return _truncater.visibleRanges;
}
@end

View File

@@ -0,0 +1,70 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <UIKit/UIKit.h>
/**
* @abstract an immutable class for calculating shadow padding drawing a shadowed background for text
*/
@interface ASTextKitShadower : NSObject
- (instancetype)initWithShadowOffset:(CGSize)shadowOffset
shadowColor:(UIColor *)shadowColor
shadowOpacity:(CGFloat)shadowOpacity
shadowRadius:(CGFloat)shadowRadius;
/**
* @abstract The offset from the top-left corner at which the shadow starts.
* @discussion A positive width will move the shadow to the right.
* A positive height will move the shadow downwards.
*/
@property (nonatomic, readonly, assign) CGSize shadowOffset;
//! CGColor in which the shadow is drawn
@property (nonatomic, readonly, strong) UIColor *shadowColor;
//! Alpha of the shadow
@property (nonatomic, readonly, assign) CGFloat shadowOpacity;
//! Radius, in pixels
@property (nonatomic, readonly, assign) CGFloat shadowRadius;
/**
* @abstract The edge insets which represent shadow padding
* @discussion Each edge inset is less than or equal to zero.
*
* Example:
* CGRect boundsWithoutShadowPadding; // Large enough to fit text, not large enough to fit the shadow as well
* UIEdgeInsets shadowPadding = [shadower shadowPadding];
* CGRect boundsWithShadowPadding = UIEdgeInsetsRect(boundsWithoutShadowPadding, shadowPadding);
*/
- (UIEdgeInsets)shadowPadding;
- (CGSize)insetSizeWithConstrainedSize:(CGSize)constrainedSize;
- (CGRect)insetRectWithConstrainedRect:(CGRect)constrainedRect;
- (CGSize)outsetSizeWithInsetSize:(CGSize)insetSize;
- (CGRect)outsetRectWithInsetRect:(CGRect)insetRect;
- (CGRect)offsetRectWithInternalRect:(CGRect)internalRect;
- (CGPoint)offsetPointWithInternalPoint:(CGPoint)internalPoint;
- (CGPoint)offsetPointWithExternalPoint:(CGPoint)externalPoint;
/**
* @abstract draws the shadow for text in the provided CGContext
* @discussion Call within the text node's +drawRect method
*/
- (void)setShadowInContext:(CGContextRef)context;
@end

View File

@@ -0,0 +1,148 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASTextKitShadower.h"
static inline CGSize _insetSize(CGSize size, UIEdgeInsets insets)
{
return UIEdgeInsetsInsetRect({.size = size}, insets).size;
}
static inline UIEdgeInsets _invertInsets(UIEdgeInsets insets)
{
return {
.top = -insets.top,
.left = -insets.left,
.bottom = -insets.bottom,
.right = -insets.right
};
}
@implementation ASTextKitShadower {
UIEdgeInsets _calculatedShadowPadding;
}
- (instancetype)initWithShadowOffset:(CGSize)shadowOffset
shadowColor:(UIColor *)shadowColor
shadowOpacity:(CGFloat)shadowOpacity
shadowRadius:(CGFloat)shadowRadius
{
if (self = [super init]) {
_shadowOffset = shadowOffset;
_shadowColor = shadowColor;
_shadowOpacity = shadowOpacity;
_shadowRadius = shadowRadius;
_calculatedShadowPadding = UIEdgeInsetsMake(-INFINITY, -INFINITY, INFINITY, INFINITY);
}
return self;
}
/*
* This method is duplicated here because it gets called frequently, and we were
* wasting valuable time constructing a state object to ask it.
*/
- (BOOL)_shouldDrawShadow
{
return _shadowOpacity != 0.0 && _shadowColor != nil && (_shadowRadius != 0 || !CGSizeEqualToSize(_shadowOffset, CGSizeZero));
}
- (void)setShadowInContext:(CGContextRef)context
{
if ([self _shouldDrawShadow]) {
CGColorRef textShadowColor = CGColorRetain(_shadowColor.CGColor);
CGSize textShadowOffset = _shadowOffset;
CGFloat textShadowOpacity = _shadowOpacity;
CGFloat textShadowRadius = _shadowRadius;
if (textShadowOpacity != 1.0) {
CGFloat inherentAlpha = CGColorGetAlpha(textShadowColor);
CGColorRef oldTextShadowColor = textShadowColor;
textShadowColor = CGColorCreateCopyWithAlpha(textShadowColor, inherentAlpha * textShadowOpacity);
CGColorRelease(oldTextShadowColor);
}
CGContextSetShadowWithColor(context, textShadowOffset, textShadowRadius, textShadowColor);
CGColorRelease(textShadowColor);
}
}
- (UIEdgeInsets)shadowPadding
{
if (_calculatedShadowPadding.top == -INFINITY) {
if (![self _shouldDrawShadow]) {
return UIEdgeInsetsZero;
}
UIEdgeInsets shadowPadding = UIEdgeInsetsZero;
// min values are expected to be negative for most typical shadowOffset and
// blurRadius settings:
shadowPadding.top = fminf(0.0f, _shadowOffset.height - _shadowRadius);
shadowPadding.left = fminf(0.0f, _shadowOffset.width - _shadowRadius);
shadowPadding.bottom = fminf(0.0f, -_shadowOffset.height - _shadowRadius);
shadowPadding.right = fminf(0.0f, -_shadowOffset.width - _shadowRadius);
_calculatedShadowPadding = shadowPadding;
}
return _calculatedShadowPadding;
}
- (CGSize)insetSizeWithConstrainedSize:(CGSize)constrainedSize
{
return _insetSize(constrainedSize, _invertInsets([self shadowPadding]));
}
- (CGRect)insetRectWithConstrainedRect:(CGRect)constrainedRect
{
return UIEdgeInsetsInsetRect(constrainedRect, _invertInsets([self shadowPadding]));
}
- (CGSize)outsetSizeWithInsetSize:(CGSize)insetSize
{
return _insetSize(insetSize, [self shadowPadding]);
}
- (CGRect)outsetRectWithInsetRect:(CGRect)insetRect
{
return UIEdgeInsetsInsetRect(insetRect, [self shadowPadding]);
}
- (CGRect)offsetRectWithInternalRect:(CGRect)internalRect
{
return (CGRect){
.origin = [self offsetPointWithInternalPoint:internalRect.origin],
.size = internalRect.size
};
}
- (CGPoint)offsetPointWithInternalPoint:(CGPoint)internalPoint
{
UIEdgeInsets shadowPadding = [self shadowPadding];
return (CGPoint){
internalPoint.x + shadowPadding.left,
internalPoint.y + shadowPadding.top
};
}
- (CGPoint)offsetPointWithExternalPoint:(CGPoint)externalPoint
{
UIEdgeInsets shadowPadding = [self shadowPadding];
return (CGPoint){
externalPoint.x - shadowPadding.left,
externalPoint.y - shadowPadding.top
};
}
@end

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <UIKit/UIKit.h>
#import "ASTextKitTruncating.h"
@interface ASTextKitTailTruncater : NSObject <ASTextKitTruncating>
@end

View File

@@ -0,0 +1,191 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "ASAssert.h"
#import "ASTextKitContext.h"
#import "ASTextKitTailTruncater.h"
@implementation ASTextKitTailTruncater
{
__weak ASTextKitContext *_context;
NSAttributedString *_truncationAttributedString;
NSCharacterSet *_avoidTailTruncationSet;
CGSize _constrainedSize;
}
@synthesize visibleRanges = _visibleRanges;
@synthesize truncationStringRect = _truncationStringRect;
- (instancetype)initWithContext:(ASTextKitContext *)context
truncationAttributedString:(NSAttributedString *)truncationAttributedString
avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet
constrainedSize:(CGSize)constrainedSize
{
if (self = [super init]) {
_context = context;
_truncationAttributedString = truncationAttributedString;
_avoidTailTruncationSet = avoidTailTruncationSet;
_constrainedSize = constrainedSize;
[self _truncate];
}
return self;
}
/**
Calculates the intersection of the truncation message within the end of the last line.
*/
- (NSUInteger)_calculateCharacterIndexBeforeTruncationMessage:(NSLayoutManager *)layoutManager
textStorage:(NSTextStorage *)textStorage
textContainer:(NSTextContainer *)textContainer
{
CGRect constrainedRect = (CGRect){ .size = textContainer.size };
NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:constrainedRect
inTextContainer:textContainer];
NSInteger lastVisibleGlyphIndex = (NSMaxRange(visibleGlyphRange) - 1);
if (lastVisibleGlyphIndex < 0) {
return NSNotFound;
}
CGRect lastLineRect = [layoutManager lineFragmentRectForGlyphAtIndex:lastVisibleGlyphIndex
effectiveRange:NULL];
CGRect lastLineUsedRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:lastVisibleGlyphIndex
effectiveRange:NULL];
NSParagraphStyle *paragraphStyle = [textStorage attributesAtIndex:[layoutManager characterIndexForGlyphAtIndex:lastVisibleGlyphIndex]
effectiveRange:NULL][NSParagraphStyleAttributeName];
// We assume LTR so long as the writing direction is not
BOOL rtlWritingDirection = paragraphStyle ? paragraphStyle.baseWritingDirection == NSWritingDirectionRightToLeft : NO;
// We only want to treat the trunction rect as left-aligned in the case that we are right-aligned and our writing
// direction is RTL.
BOOL leftAligned = CGRectGetMinX(lastLineRect) == CGRectGetMinX(lastLineUsedRect) || !rtlWritingDirection;
// Calculate the bounding rectangle for the truncation message
ASTextKitContext *truncationContext = [[ASTextKitContext alloc] initWithAttributedString:_truncationAttributedString
lineBreakMode:NSLineBreakByWordWrapping
maximumNumberOfLines:1
exclusionPaths:nil
constrainedSize:constrainedRect.size
layoutManagerFactory:nil];
__block CGRect truncationUsedRect;
[truncationContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *truncationLayoutManager, NSTextStorage *truncationTextStorage, NSTextContainer *truncationTextContainer) {
// Size the truncation message
[truncationLayoutManager ensureLayoutForTextContainer:truncationTextContainer];
NSRange truncationGlyphRange = [truncationLayoutManager glyphRangeForTextContainer:truncationTextContainer];
truncationUsedRect = [truncationLayoutManager boundingRectForGlyphRange:truncationGlyphRange
inTextContainer:truncationTextContainer];
}];
CGFloat truncationOriginX = (leftAligned ?
CGRectGetMaxX(constrainedRect) - truncationUsedRect.size.width :
CGRectGetMinX(constrainedRect));
CGRect translatedTruncationRect = CGRectMake(truncationOriginX,
CGRectGetMinY(lastLineRect),
truncationUsedRect.size.width,
truncationUsedRect.size.height);
// Determine which glyph is the first to be clipped / overlaps the truncation message.
CGFloat truncationMessageX = (leftAligned ?
CGRectGetMinX(translatedTruncationRect) :
CGRectGetMaxX(translatedTruncationRect));
CGPoint beginningOfTruncationMessage = CGPointMake(truncationMessageX,
CGRectGetMidY(translatedTruncationRect));
NSUInteger firstClippedGlyphIndex = [layoutManager glyphIndexForPoint:beginningOfTruncationMessage
inTextContainer:textContainer
fractionOfDistanceThroughGlyph:NULL];
// If it didn't intersect with any text then it should just return the last visible character index, since the
// truncation rect can fully fit on the line without clipping any other text.
if (firstClippedGlyphIndex == NSNotFound) {
return [layoutManager characterIndexForGlyphAtIndex:lastVisibleGlyphIndex];
}
NSUInteger firstCharacterIndexToReplace = [layoutManager characterIndexForGlyphAtIndex:firstClippedGlyphIndex];
// Break on word boundaries
return [self _findTruncationInsertionPointAtOrBeforeCharacterIndex:firstCharacterIndexToReplace
layoutManager:layoutManager
textStorage:textStorage];
}
/**
Finds the first whitespace at or before the character index do we don't truncate in the middle of words
If there are multiple whitespaces together (say a space and a newline), this will backtrack to the first one
*/
- (NSUInteger)_findTruncationInsertionPointAtOrBeforeCharacterIndex:(NSUInteger)firstCharacterIndexToReplace
layoutManager:(NSLayoutManager *)layoutManager
textStorage:(NSTextStorage *)textStorage
{
// Don't attempt to truncate beyond the end of the string
if (firstCharacterIndexToReplace >= textStorage.length) {
return 0;
}
// Find the glyph range of the line fragment containing the first character to replace.
NSRange lineGlyphRange;
[layoutManager lineFragmentRectForGlyphAtIndex:[layoutManager glyphIndexForCharacterAtIndex:firstCharacterIndexToReplace]
effectiveRange:&lineGlyphRange];
// Look for the first whitespace from the end of the line, starting from the truncation point
NSUInteger startingSearchIndex = [layoutManager characterIndexForGlyphAtIndex:lineGlyphRange.location];
NSUInteger endingSearchIndex = firstCharacterIndexToReplace;
NSRange rangeToSearch = NSMakeRange(startingSearchIndex, (endingSearchIndex - startingSearchIndex));
NSRange rangeOfLastVisibleAvoidedChars = { .location = NSNotFound };
if (_avoidTailTruncationSet) {
rangeOfLastVisibleAvoidedChars = [textStorage.string rangeOfCharacterFromSet:_avoidTailTruncationSet
options:NSBackwardsSearch
range:rangeToSearch];
}
// Couldn't find a good place to truncate. Might be because there is no whitespace in the text, or we're dealing
// with a foreign language encoding. Settle for truncating at the original place, which may be mid-word.
if (rangeOfLastVisibleAvoidedChars.location == NSNotFound) {
return firstCharacterIndexToReplace;
} else {
return rangeOfLastVisibleAvoidedChars.location;
}
}
- (void)_truncate
{
[_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
NSUInteger originalStringLength = textStorage.length;
[layoutManager ensureLayoutForTextContainer:textContainer];
NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:{ .size = textContainer.size }
inTextContainer:textContainer];
NSRange visibleCharacterRange = [layoutManager characterRangeForGlyphRange:visibleGlyphRange
actualGlyphRange:NULL];
// Check if text is truncated, and if so apply our truncation string
if (visibleCharacterRange.length < originalStringLength && _truncationAttributedString.length > 0) {
NSInteger firstCharacterIndexToReplace = [self _calculateCharacterIndexBeforeTruncationMessage:layoutManager
textStorage:textStorage
textContainer:textContainer];
if (firstCharacterIndexToReplace == 0 || firstCharacterIndexToReplace == NSNotFound) {
return;
}
// Update/truncate the visible range of text
visibleCharacterRange = NSMakeRange(0, firstCharacterIndexToReplace);
NSRange truncationReplacementRange = NSMakeRange(firstCharacterIndexToReplace,
textStorage.length - firstCharacterIndexToReplace);
// Replace the end of the visible message with the truncation string
[textStorage replaceCharactersInRange:truncationReplacementRange
withAttributedString:_truncationAttributedString];
}
_visibleRanges = { visibleCharacterRange };
}];
}
@end

View File

@@ -0,0 +1,37 @@
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <vector>
#import <UIKit/UIKit.h>
#import "ASTextKitRenderer.h"
@protocol ASTextKitTruncating <NSObject>
@property (nonatomic, assign, readonly) std::vector<NSRange> visibleRanges;
@property (nonatomic, assign, readonly) CGRect truncationStringRect;
/**
A truncater object is initialized with the full state of the text. It is a Single Responsibility Object that is
mutative. It configures the state of the TextKit components (layout manager, text container, text storage) to achieve
the intended truncation, then it stores the resulting state for later fetching.
The truncater may mutate the state of the text storage such that only the drawn string is actually present in the
text storage itself.
The truncater should not store a strong reference to the context to prevent retain cycles.
*/
- (instancetype)initWithContext:(ASTextKitContext *)context
truncationAttributedString:(NSAttributedString *)truncationAttributedString
avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet
constrainedSize:(CGSize)constrainedSize;
@end

View File

@@ -0,0 +1,12 @@
/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#pragma once
// Use this attribute name to add "word kerning"
static NSString *const ASTextNodeWordKerningAttributeName = @"ASAttributedStringWordKerning";

View File

@@ -0,0 +1,33 @@
/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#import <UIKit/NSLayoutManager.h>
NS_ASSUME_NONNULL_BEGIN
/**
@abstract This class acts as the NSLayoutManagerDelegate for ASTextNode.
@discussion Its current job is word kerning, i.e. adjusting the width of spaces to match the set
wordKernedSpaceWidth. If word kerning is not needed, set the layoutManager's delegate to nil.
*/
@interface ASTextNodeWordKerner : NSObject <NSLayoutManagerDelegate>
/**
The following @optional NSLayoutManagerDelegate methods are implemented:
- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)props characterIndexes:(const NSUInteger *)charIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange NS_AVAILABLE_IOS(7_0);
- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)action forControlCharacterAtIndex:(NSUInteger)charIndex NS_AVAILABLE_IOS(7_0);
- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)charIndex NS_AVAILABLE_IOS(7_0);
*/
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,129 @@
/* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "ASTextNodeWordKerner.h"
#import <UIKit/UIKit.h>
#import "ASTextNodeTypes.h"
@implementation ASTextNodeWordKerner
#pragma mark - NSLayoutManager Delegate
- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)properties characterIndexes:(const NSUInteger *)characterIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange
{
NSUInteger glyphCount = glyphRange.length;
NSGlyphProperty *newGlyphProperties = NULL;
BOOL usesWordKerning = NO;
// If our typing attributes specify word kerning, specify the spaces as whitespace control characters so we can customize their width.
// Are any of the characters spaces?
NSString *textStorageString = layoutManager.textStorage.string;
for (NSUInteger arrayIndex = 0; arrayIndex < glyphCount; arrayIndex++) {
NSUInteger characterIndex = characterIndexes[arrayIndex];
if ([textStorageString characterAtIndex:characterIndex] != ' ')
continue;
// If we've set the whitespace control character for this space already, we have nothing to do.
if (properties[arrayIndex] == NSGlyphPropertyControlCharacter) {
usesWordKerning = YES;
continue;
}
// Create new glyph properties, if necessary.
if (!newGlyphProperties) {
newGlyphProperties = (NSGlyphProperty *)malloc(sizeof(NSGlyphProperty) * glyphCount);
memcpy(newGlyphProperties, properties, (sizeof(NSGlyphProperty) * glyphCount));
}
// It's a space. Make it a whitespace control character.
newGlyphProperties[arrayIndex] = NSGlyphPropertyControlCharacter;
}
// If we don't have any custom glyph properties, return 0 to indicate to the layout manager that it should use the standard glyphs+properties.
if (!newGlyphProperties) {
if (usesWordKerning) {
// If the text does use word kerning we have to make sure we return the correct glyphCount, or the layout manager will just use the default properties and ignore our kerning.
[layoutManager setGlyphs:glyphs properties:properties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange];
return glyphCount;
} else {
return 0;
}
}
// Otherwise, use our custom glyph properties.
[layoutManager setGlyphs:glyphs properties:newGlyphProperties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange];
free(newGlyphProperties);
return glyphCount;
}
- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)defaultAction forControlCharacterAtIndex:(NSUInteger)characterIndex
{
// If it's a space character and we have custom word kerning, use the whitespace action control character.
if ([layoutManager.textStorage.string characterAtIndex:characterIndex] == ' ')
return NSControlCharacterWhitespaceAction;
return defaultAction;
}
- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)characterIndex
{
CGFloat wordKernedSpaceWidth = [self _wordKernedSpaceWidthForCharacterAtIndex:characterIndex atGlyphPosition:glyphPosition forTextContainer:textContainer layoutManager:layoutManager];
return CGRectMake(glyphPosition.x, glyphPosition.y, wordKernedSpaceWidth, CGRectGetHeight(proposedRect));
}
- (CGFloat)_wordKernedSpaceWidthForCharacterAtIndex:(NSUInteger)characterIndex atGlyphPosition:(CGPoint)glyphPosition forTextContainer:(NSTextContainer *)textContainer layoutManager:(NSLayoutManager *)layoutManager
{
// We use a map table for pointer equality and non-copying keys.
static NSMapTable *spaceSizes;
// NSMapTable is a defined thread unsafe class, so we need to synchronize
// access in a light manner. So we use dispatch_sync on this queue for all
// access to the map table.
static dispatch_queue_t mapQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
spaceSizes = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory capacity:1];
mapQueue = dispatch_queue_create("org.AsyncDisplayKit.wordKerningQueue", DISPATCH_QUEUE_SERIAL);
});
CGFloat ordinarySpaceWidth;
UIFont *font = [layoutManager.textStorage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL];
CGFloat wordKerning = [[layoutManager.textStorage attribute:ASTextNodeWordKerningAttributeName atIndex:characterIndex effectiveRange:NULL] floatValue];
__block NSNumber *ordinarySpaceSizeValue;
dispatch_sync(mapQueue, ^{
ordinarySpaceSizeValue = [spaceSizes objectForKey:font];
});
if (ordinarySpaceSizeValue == nil) {
ordinarySpaceWidth = [@" " sizeWithAttributes:@{ NSFontAttributeName : font }].width;
dispatch_async(mapQueue, ^{
[spaceSizes setObject:@(ordinarySpaceWidth) forKey:font];
});
} else {
ordinarySpaceWidth = [ordinarySpaceSizeValue floatValue];
}
CGFloat totalKernedWidth = (ordinarySpaceWidth + wordKerning);
// TextKit normally handles whitespace by increasing the advance of the previous glyph, rather than displaying an
// actual glyph for the whitespace itself. However, in order to implement word kerning, we explicitly require a
// discrete glyph whose bounding box we can specify. The problem is that TextKit does not know this glyph is
// invisible. From TextKit's perspective, this whitespace glyph is a glyph that MUST be displayed. Thus when it
// comes to determining linebreaks, the width of this trailing whitespace glyph is considered. This causes
// our text to wrap sooner than it otherwise would, as room is allocated at the end of each line for a glyph that
// isn't actually visible. To implement our desired behavior, we check to see if the current whitespace glyph
// would break to the next line. If it breaks to the next line, then this constitutes trailing whitespace, and
// we specify enough room to fill up the remainder of the line, but nothing more.
if (glyphPosition.x + totalKernedWidth > textContainer.size.width) {
return (textContainer.size.width - glyphPosition.x);
}
return totalKernedWidth;
}
@end