|
15 | 15 | #import <React/NSTextStorage+FontScaling.h> |
16 | 16 | #import <React/RCTUtils.h> |
17 | 17 | #import <react/featureflags/ReactNativeFeatureFlags.h> |
| 18 | +#import <react/renderer/textlayoutmanager/TextMeasureCache.h> |
18 | 19 | #import <react/renderer/textlayoutmanager/TextMeasurementRounding.h> |
19 | 20 | #import <react/utils/ManagedObjectWrapper.h> |
20 | 21 | #import <react/utils/SimpleThreadSafeCache.h> |
21 | 22 |
|
22 | 23 | using namespace facebook::react; |
23 | 24 |
|
| 25 | +namespace facebook::react { |
| 26 | + |
| 27 | +/* |
| 28 | + * Identifies the text storage a text view draws: what it draws, and the size of |
| 29 | + * the frame it draws it in. Two paragraphs that are equivalent this way draw |
| 30 | + * the same, so they share a text storage. Equivalence is the one prepared text |
| 31 | + * layouts use on Android: everything drawing looks at, and nothing that only |
| 32 | + * matters for hit testing, such as a fragment's event emitter. |
| 33 | + * |
| 34 | + * The frame size is all a layout depends on: it is the size the view draws in, |
| 35 | + * already rounded to the pixel grid by Yoga, so the pixel scale factor is not |
| 36 | + * part of the key. |
| 37 | + */ |
| 38 | +class RCTPreparedTextLayoutKey final { |
| 39 | + public: |
| 40 | + AttributedString attributedString; |
| 41 | + ParagraphAttributes paragraphAttributes; |
| 42 | + Size size; |
| 43 | + |
| 44 | + bool operator==(const RCTPreparedTextLayoutKey &rhs) const |
| 45 | + { |
| 46 | + return areAttributedStringsEquivalentDisplayWise(attributedString, rhs.attributedString) && |
| 47 | + paragraphAttributes == rhs.paragraphAttributes && size == rhs.size; |
| 48 | + } |
| 49 | +}; |
| 50 | + |
| 51 | +inline RCTPreparedTextLayoutKey RCTPreparedTextLayoutKeyFor( |
| 52 | + const AttributedString &attributedString, |
| 53 | + const ParagraphAttributes ¶graphAttributes, |
| 54 | + CGSize size) |
| 55 | +{ |
| 56 | + return RCTPreparedTextLayoutKey{ |
| 57 | + .attributedString = attributedString, |
| 58 | + .paragraphAttributes = paragraphAttributes, |
| 59 | + .size = facebook::react::Size{ |
| 60 | + .width = static_cast<facebook::react::Float>(size.width), |
| 61 | + .height = static_cast<facebook::react::Float>(size.height)}}; |
| 62 | +} |
| 63 | + |
| 64 | +} // namespace facebook::react |
| 65 | + |
| 66 | +namespace std { |
| 67 | + |
| 68 | +template <> |
| 69 | +struct hash<facebook::react::RCTPreparedTextLayoutKey> { |
| 70 | + size_t operator()(const facebook::react::RCTPreparedTextLayoutKey &key) const |
| 71 | + { |
| 72 | + return facebook::react::hash_combine( |
| 73 | + facebook::react::attributedStringHashDisplayWise(key.attributedString), key.paragraphAttributes, key.size); |
| 74 | + } |
| 75 | +}; |
| 76 | + |
| 77 | +} // namespace std |
| 78 | + |
24 | 79 | @implementation RCTTextLayoutManager { |
25 | 80 | SimpleThreadSafeCache<AttributedString, std::shared_ptr<void>, 256> _cache; |
| 81 | + /* |
| 82 | + * Text laid out for the first draw of a text view. The text storage is the |
| 83 | + * prepared layout: it owns the layout manager that holds the lines. |
| 84 | + */ |
| 85 | + SimpleThreadSafeCache<RCTPreparedTextLayoutKey, std::shared_ptr<void>, 256> _preparedTextLayouts; |
26 | 86 | } |
27 | 87 |
|
28 | 88 | static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsizeMode) |
@@ -128,15 +188,36 @@ - (CGRect)drawingFrameForAttributedString:(const AttributedString &)attributedSt |
128 | 188 | return drawingFrame; |
129 | 189 | } |
130 | 190 |
|
| 191 | +- (void)prepareAttributedStringForDrawing:(const AttributedString &)attributedString |
| 192 | + paragraphAttributes:(ParagraphAttributes)paragraphAttributes |
| 193 | + size:(CGSize)size |
| 194 | +{ |
| 195 | + if (attributedString.isEmpty() || size.width <= 0 || size.height <= 0) { |
| 196 | + return; |
| 197 | + } |
| 198 | + |
| 199 | + // Laying the text out here holds the cache for the whole layout, which |
| 200 | + // serializes preparations. Drawing only looks layouts up, so it never waits on |
| 201 | + // this. |
| 202 | + (void)_preparedTextLayouts.get(RCTPreparedTextLayoutKeyFor(attributedString, paragraphAttributes, size), [&]() { |
| 203 | + NSAttributedString *nsAttributedString = [self _nsAttributedStringFromAttributedString:attributedString]; |
| 204 | + NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:nsAttributedString |
| 205 | + paragraphAttributes:paragraphAttributes |
| 206 | + size:size]; |
| 207 | + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; |
| 208 | + [layoutManager ensureLayoutForTextContainer:layoutManager.textContainers.firstObject]; |
| 209 | + return wrapManagedObject(textStorage); |
| 210 | + }); |
| 211 | +} |
| 212 | + |
131 | 213 | - (void)drawAttributedString:(const AttributedString &)attributedString |
132 | 214 | paragraphAttributes:(ParagraphAttributes)paragraphAttributes |
133 | 215 | frame:(CGRect)frame |
134 | 216 | drawHighlightPath:(void (^_Nullable)(UIBezierPath *highlightPath))block |
135 | 217 | { |
136 | | - NSTextStorage *textStorage = [self |
137 | | - _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] |
138 | | - paragraphAttributes:paragraphAttributes |
139 | | - size:frame.size]; |
| 218 | + NSTextStorage *textStorage = [self _textStorageForDrawingWithAttributedString:attributedString |
| 219 | + paragraphAttributes:paragraphAttributes |
| 220 | + size:frame.size]; |
140 | 221 | NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; |
141 | 222 | NSTextContainer *textContainer = layoutManager.textContainers.firstObject; |
142 | 223 |
|
@@ -431,6 +512,29 @@ - (CGFloat)_maximumFontSizeInAttributedString:(NSAttributedString *)attributedSt |
431 | 512 | return maximumFontSize; |
432 | 513 | } |
433 | 514 |
|
| 515 | +/* |
| 516 | + * The text storage to draw `attributedString` with in a frame of `size`: the one |
| 517 | + * prepared for exactly that, or a new one when none was prepared. |
| 518 | + */ |
| 519 | +- (NSTextStorage *)_textStorageForDrawingWithAttributedString:(const AttributedString &)attributedString |
| 520 | + paragraphAttributes:(ParagraphAttributes)paragraphAttributes |
| 521 | + size:(CGSize)size |
| 522 | +{ |
| 523 | + auto preparedTextLayout = |
| 524 | + _preparedTextLayouts.get(RCTPreparedTextLayoutKeyFor(attributedString, paragraphAttributes, size)); |
| 525 | + NSTextStorage *textStorage = preparedTextLayout.has_value() ? unwrapManagedObject(*preparedTextLayout) : nil; |
| 526 | + if (textStorage != nil) { |
| 527 | + return textStorage; |
| 528 | + } |
| 529 | + |
| 530 | + // Not prepared: lay the text out here, as before. It is not added to the |
| 531 | + // cache, so that drawing never holds it. |
| 532 | + return [self |
| 533 | + _textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString] |
| 534 | + paragraphAttributes:paragraphAttributes |
| 535 | + size:size]; |
| 536 | +} |
| 537 | + |
434 | 538 | - (NSTextStorage *)_textStorageAndLayoutManagerWithAttributesString:(NSAttributedString *)attributedString |
435 | 539 | paragraphAttributes:(ParagraphAttributes)paragraphAttributes |
436 | 540 | size:(CGSize)size |
|
0 commit comments