mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Merge branch 'master' into update-objc
Conflicts: AsyncDisplayKit/ASDisplayNodeExtras.h AsyncDisplayKit/Details/ASTextNodeRenderer.h AsyncDisplayKit/Details/ASTextNodeShadower.h
This commit is contained in:
171
AsyncDisplayKit/TextKit/ASEqualityHashHelpers.h
Normal file
171
AsyncDisplayKit/TextKit/ASEqualityHashHelpers.h
Normal 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);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
129
AsyncDisplayKit/TextKit/ASTextKitAttributes.h
Executable file
129
AsyncDisplayKit/TextKit/ASTextKitAttributes.h
Executable 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
|
||||
37
AsyncDisplayKit/TextKit/ASTextKitAttributes.mm
Executable file
37
AsyncDisplayKit/TextKit/ASTextKitAttributes.mm
Executable 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]));
|
||||
}
|
||||
46
AsyncDisplayKit/TextKit/ASTextKitContext.h
Executable file
46
AsyncDisplayKit/TextKit/ASTextKitContext.h
Executable 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
|
||||
60
AsyncDisplayKit/TextKit/ASTextKitContext.mm
Executable file
60
AsyncDisplayKit/TextKit/ASTextKitContext.mm
Executable 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
|
||||
86
AsyncDisplayKit/TextKit/ASTextKitCoreTextAdditions.h
Normal file
86
AsyncDisplayKit/TextKit/ASTextKitCoreTextAdditions.h
Normal 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
|
||||
246
AsyncDisplayKit/TextKit/ASTextKitCoreTextAdditions.m
Normal file
246
AsyncDisplayKit/TextKit/ASTextKitCoreTextAdditions.m
Normal 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), ¶graphSpacing))
|
||||
newParagraphStyle.paragraphSpacing = paragraphSpacing;
|
||||
|
||||
// kCTParagraphStyleSpecifierParagraphSpacingBefore -> paragraphSpacingBefore
|
||||
CGFloat paragraphSpacingBefore;
|
||||
if (CTParagraphStyleGetValueForSpecifier(coreTextParagraphStyle, kCTParagraphStyleSpecifierParagraphSpacingBefore, sizeof(paragraphSpacingBefore), ¶graphSpacingBefore))
|
||||
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
|
||||
28
AsyncDisplayKit/TextKit/ASTextKitEntityAttribute.h
Executable file
28
AsyncDisplayKit/TextKit/ASTextKitEntityAttribute.h
Executable 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
|
||||
40
AsyncDisplayKit/TextKit/ASTextKitEntityAttribute.m
Executable file
40
AsyncDisplayKit/TextKit/ASTextKitEntityAttribute.m
Executable 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
|
||||
58
AsyncDisplayKit/TextKit/ASTextKitHelpers.h
Normal file
58
AsyncDisplayKit/TextKit/ASTextKitHelpers.h
Normal 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
|
||||
57
AsyncDisplayKit/TextKit/ASTextKitHelpers.mm
Normal file
57
AsyncDisplayKit/TextKit/ASTextKitHelpers.mm
Normal 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
|
||||
103
AsyncDisplayKit/TextKit/ASTextKitRenderer+Positioning.h
Executable file
103
AsyncDisplayKit/TextKit/ASTextKitRenderer+Positioning.h
Executable 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
|
||||
374
AsyncDisplayKit/TextKit/ASTextKitRenderer+Positioning.mm
Executable file
374
AsyncDisplayKit/TextKit/ASTextKitRenderer+Positioning.mm
Executable 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
|
||||
29
AsyncDisplayKit/TextKit/ASTextKitRenderer+TextChecking.h
Executable file
29
AsyncDisplayKit/TextKit/ASTextKitRenderer+TextChecking.h
Executable 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
|
||||
102
AsyncDisplayKit/TextKit/ASTextKitRenderer+TextChecking.mm
Executable file
102
AsyncDisplayKit/TextKit/ASTextKitRenderer+TextChecking.mm
Executable 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
|
||||
84
AsyncDisplayKit/TextKit/ASTextKitRenderer.h
Executable file
84
AsyncDisplayKit/TextKit/ASTextKitRenderer.h
Executable 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
|
||||
141
AsyncDisplayKit/TextKit/ASTextKitRenderer.mm
Executable file
141
AsyncDisplayKit/TextKit/ASTextKitRenderer.mm
Executable 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
|
||||
70
AsyncDisplayKit/TextKit/ASTextKitShadower.h
Executable file
70
AsyncDisplayKit/TextKit/ASTextKitShadower.h
Executable 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
|
||||
148
AsyncDisplayKit/TextKit/ASTextKitShadower.mm
Executable file
148
AsyncDisplayKit/TextKit/ASTextKitShadower.mm
Executable 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
|
||||
17
AsyncDisplayKit/TextKit/ASTextKitTailTruncater.h
Executable file
17
AsyncDisplayKit/TextKit/ASTextKitTailTruncater.h
Executable 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
|
||||
191
AsyncDisplayKit/TextKit/ASTextKitTailTruncater.mm
Executable file
191
AsyncDisplayKit/TextKit/ASTextKitTailTruncater.mm
Executable 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
|
||||
37
AsyncDisplayKit/TextKit/ASTextKitTruncating.h
Executable file
37
AsyncDisplayKit/TextKit/ASTextKitTruncating.h
Executable 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
|
||||
12
AsyncDisplayKit/TextKit/ASTextNodeTypes.h
Normal file
12
AsyncDisplayKit/TextKit/ASTextNodeTypes.h
Normal 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";
|
||||
33
AsyncDisplayKit/TextKit/ASTextNodeWordKerner.h
Normal file
33
AsyncDisplayKit/TextKit/ASTextNodeWordKerner.h
Normal 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
|
||||
129
AsyncDisplayKit/TextKit/ASTextNodeWordKerner.m
Normal file
129
AsyncDisplayKit/TextKit/ASTextNodeWordKerner.m
Normal 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
|
||||
Reference in New Issue
Block a user