|
46 | 46 | // Distance to the bottom of the baseline, for text attachment baseline layout, NSNumber value |
47 | 47 | NSAttributedStringKey const HippyVerticalAlignBaselineOffsetAttributeName = @"HippyVerticalAlignBaselineOffsetAttributeName"; |
48 | 48 |
|
| 49 | +// Keys for pending attributes collection (avoid string hardcode) |
| 50 | +static NSString *const HippyPendingRangeKey = @"HippyPendingRangeKey"; |
| 51 | +static NSString *const HippyPendingOffsetKey = @"HippyPendingOffsetKey"; |
| 52 | +static NSString *const HippyPendingValueKey = @"HippyPendingValueKey"; |
| 53 | + |
49 | 54 |
|
50 | 55 | CGFloat const HippyTextAutoSizeWidthErrorMargin = 0.05; |
51 | 56 | CGFloat const HippyTextAutoSizeHeightErrorMargin = 0.025; |
@@ -91,6 +96,9 @@ @interface HippyShadowText () <NSLayoutManagerDelegate> |
91 | 96 | BOOL _isNestedText; // Indicates whether Text is nested, for speeding up typesetting calculations |
92 | 97 | BOOL _needRelayoutText; // special styles require two layouts, eg. verticalAlign etc |
93 | 98 | hippy::LayoutMeasureMode _cachedTextStorageWidthMode; // cached width mode when building text storage |
| 99 | + // Collect pending edits to avoid mutating textStorage during layout callbacks |
| 100 | + NSMutableArray<NSDictionary *> *_pendingBaselineOffsets; // entries: { range:NSValue(NSRange), offset:NSNumber } |
| 101 | + NSMutableArray<NSDictionary *> *_pendingAttachmentBaselineBottoms; // entries: { range:NSValue(NSRange), value:NSNumber } |
94 | 102 | } |
95 | 103 |
|
96 | 104 | @end |
@@ -171,6 +179,8 @@ - (instancetype)init { |
171 | 179 | if (NSWritingDirectionRightToLeft == [[HippyI18nUtils sharedInstance] writingDirectionForCurrentAppLanguage]) { |
172 | 180 | self.textAlign = NSTextAlignmentRight; |
173 | 181 | } |
| 182 | + _pendingBaselineOffsets = [NSMutableArray array]; |
| 183 | + _pendingAttachmentBaselineBottoms = [NSMutableArray array]; |
174 | 184 | } |
175 | 185 | return self; |
176 | 186 | } |
@@ -391,15 +401,49 @@ - (NSTextStorage *)buildTextStorageForWidth:(CGFloat)width widthMode:(hippy::Lay |
391 | 401 |
|
392 | 402 | layoutManager.delegate = self; |
393 | 403 | [layoutManager addTextContainer:textContainer]; |
| 404 | + // start clean collection for this layout build |
| 405 | + [_pendingBaselineOffsets removeAllObjects]; |
| 406 | + [_pendingAttachmentBaselineBottoms removeAllObjects]; |
394 | 407 | [layoutManager ensureLayoutForTextContainer:textContainer]; |
| 408 | + |
| 409 | + // Apply pending attributes collected during the first layout pass. |
| 410 | + if (_pendingBaselineOffsets.count > 0 || _pendingAttachmentBaselineBottoms.count > 0) { |
| 411 | + [textStorage beginEditing]; |
| 412 | + NSUInteger const storageLength = textStorage.length; |
| 413 | + for (NSDictionary *entry in _pendingBaselineOffsets) { |
| 414 | + NSValue *rangeValue = entry[HippyPendingRangeKey]; |
| 415 | + NSNumber *offsetValue = entry[HippyPendingOffsetKey]; |
| 416 | + if (!rangeValue || !offsetValue) { continue; } |
| 417 | + NSRange r = rangeValue.rangeValue; |
| 418 | + if (r.location == NSNotFound || r.length == 0) { continue; } |
| 419 | + if (NSMaxRange(r) > storageLength) { continue; } |
| 420 | + [textStorage addAttribute:NSBaselineOffsetAttributeName value:offsetValue range:r]; |
| 421 | + } |
| 422 | + for (NSDictionary *entry in _pendingAttachmentBaselineBottoms) { |
| 423 | + NSValue *rangeValue = entry[HippyPendingRangeKey]; |
| 424 | + NSNumber *value = entry[HippyPendingValueKey]; |
| 425 | + if (!rangeValue || !value) { continue; } |
| 426 | + NSRange r = rangeValue.rangeValue; |
| 427 | + if (r.location == NSNotFound || r.length == 0) { continue; } |
| 428 | + if (NSMaxRange(r) > storageLength) { continue; } |
| 429 | + [textStorage addAttribute:HippyVerticalAlignBaselineOffsetAttributeName value:value range:r]; |
| 430 | + } |
| 431 | + [textStorage endEditing]; |
| 432 | + [_pendingBaselineOffsets removeAllObjects]; |
| 433 | + [_pendingAttachmentBaselineBottoms removeAllObjects]; |
| 434 | + _needRelayoutText = YES; |
| 435 | + } |
395 | 436 |
|
396 | 437 | // for better perf, only do relayout when MeasureMode is MeasureModeExactly |
397 | 438 | if (_needRelayoutText && hippy::LayoutMeasureMode::Exactly == widthMode) { |
398 | | - // relayout text |
| 439 | + // relayout text after applying pending attributes from first pass |
399 | 440 | [layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, textStorage.length) actualCharacterRange:nil]; |
400 | 441 | [layoutManager removeTextContainerAtIndex:0]; |
401 | 442 | [layoutManager addTextContainer:textContainer]; |
402 | 443 | [layoutManager ensureLayoutForTextContainer:textContainer]; |
| 444 | + // clear any collections from the relayout pass |
| 445 | + [_pendingBaselineOffsets removeAllObjects]; |
| 446 | + [_pendingAttachmentBaselineBottoms removeAllObjects]; |
403 | 447 | _needRelayoutText = NO; |
404 | 448 | } |
405 | 449 |
|
@@ -1098,9 +1142,9 @@ - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect |
1098 | 1142 | CGFloat maxTotalHeight = MAX((maxAttachmentHeight + textBaselineToBottom), maxFont.lineHeight); |
1099 | 1143 | realBaselineOffset = (CGRectGetHeight(*lineFragmentUsedRect) - maxTotalHeight) / 2.f; |
1100 | 1144 | if (hasAttachment) { |
1101 | | - [textStorage addAttribute:HippyVerticalAlignBaselineOffsetAttributeName |
1102 | | - value:@(realBaselineOffset + textBaselineToBottom) |
1103 | | - range:storageRange]; |
| 1145 | + // Defer writing attribute to avoid mutating storage during layout |
| 1146 | + [_pendingAttachmentBaselineBottoms addObject:@{ HippyPendingRangeKey: [NSValue valueWithRange:storageRange], |
| 1147 | + HippyPendingValueKey: @(realBaselineOffset + textBaselineToBottom) }]; |
1104 | 1148 | } |
1105 | 1149 | } |
1106 | 1150 |
|
@@ -1142,9 +1186,9 @@ - (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect |
1142 | 1186 | break; |
1143 | 1187 | } |
1144 | 1188 | if (abs(offset) > .0f && !attrs[HippyShadowViewAttributeName]) { |
1145 | | - // only set for Text |
1146 | | - [textStorage addAttribute:NSBaselineOffsetAttributeName value:@(offset) range:range]; |
1147 | | - _needRelayoutText = YES; |
| 1189 | + // only set for Text; defer to avoid mutation during layout |
| 1190 | + [_pendingBaselineOffsets addObject:@{ HippyPendingRangeKey: [NSValue valueWithRange:range], |
| 1191 | + HippyPendingOffsetKey: @(offset) }]; |
1148 | 1192 | } |
1149 | 1193 | } |
1150 | 1194 | }]; |
|
0 commit comments