1/* 2 * Copyright (c) Meta Platforms, Inc. and affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7 8#import <ABI47_0_0React/ABI47_0_0RCTBaseTextInputShadowView.h> 9 10#import <ABI47_0_0React/ABI47_0_0RCTBridge.h> 11#import <ABI47_0_0React/ABI47_0_0RCTShadowView+Layout.h> 12#import <ABI47_0_0React/ABI47_0_0RCTUIManager.h> 13#import <ABI47_0_0yoga/ABI47_0_0Yoga.h> 14 15#import "ABI47_0_0NSTextStorage+FontScaling.h" 16#import <ABI47_0_0React/ABI47_0_0RCTBaseTextInputView.h> 17 18@implementation ABI47_0_0RCTBaseTextInputShadowView 19{ 20 __weak ABI47_0_0RCTBridge *_bridge; 21 NSAttributedString *_Nullable _previousAttributedText; 22 BOOL _needsUpdateView; 23 NSAttributedString *_Nullable _localAttributedText; 24 CGSize _previousContentSize; 25 26 NSString *_text; 27 NSTextStorage *_textStorage; 28 NSTextContainer *_textContainer; 29 NSLayoutManager *_layoutManager; 30} 31 32- (instancetype)initWithBridge:(ABI47_0_0RCTBridge *)bridge 33{ 34 if (self = [super init]) { 35 _bridge = bridge; 36 _needsUpdateView = YES; 37 38 ABI47_0_0YGNodeSetMeasureFunc(self.yogaNode, ABI47_0_0RCTBaseTextInputShadowViewMeasure); 39 ABI47_0_0YGNodeSetBaselineFunc(self.yogaNode, ABI47_0_0RCTTextInputShadowViewBaseline); 40 } 41 42 return self; 43} 44 45- (BOOL)isYogaLeafNode 46{ 47 return YES; 48} 49 50- (void)didSetProps:(NSArray<NSString *> *)changedProps 51{ 52 [super didSetProps:changedProps]; 53 54 // `backgroundColor` and `opacity` are being applied directly to a UIView, 55 // therefore we need to exclude them from base `textAttributes`. 56 self.textAttributes.backgroundColor = nil; 57 self.textAttributes.opacity = NAN; 58} 59 60- (void)layoutSubviewsWithContext:(ABI47_0_0RCTLayoutContext)layoutContext 61{ 62 // Do nothing. 63} 64 65- (void)setLocalData:(NSObject *)localData 66{ 67 NSAttributedString *attributedText = (NSAttributedString *)localData; 68 69 if ([attributedText isEqualToAttributedString:_localAttributedText]) { 70 return; 71 } 72 73 _localAttributedText = attributedText; 74 [self dirtyLayout]; 75} 76 77- (void)dirtyLayout 78{ 79 [super dirtyLayout]; 80 _needsUpdateView = YES; 81 ABI47_0_0YGNodeMarkDirty(self.yogaNode); 82 [self invalidateContentSize]; 83} 84 85- (void)invalidateContentSize 86{ 87 if (!_onContentSizeChange) { 88 return; 89 } 90 91 CGSize maximumSize = self.layoutMetrics.frame.size; 92 93 if (_maximumNumberOfLines == 1) { 94 maximumSize.width = CGFLOAT_MAX; 95 } else { 96 maximumSize.height = CGFLOAT_MAX; 97 } 98 99 CGSize contentSize = [self sizeThatFitsMinimumSize:(CGSize)CGSizeZero maximumSize:maximumSize]; 100 101 if (CGSizeEqualToSize(_previousContentSize, contentSize)) { 102 return; 103 } 104 _previousContentSize = contentSize; 105 106 _onContentSizeChange(@{ 107 @"contentSize": @{ 108 @"height": @(contentSize.height), 109 @"width": @(contentSize.width), 110 }, 111 @"target": self.ABI47_0_0ReactTag, 112 }); 113} 114 115- (NSString *)text 116{ 117 return _text; 118} 119 120- (void)setText:(NSString *)text 121{ 122 _text = text; 123 // Clear `_previousAttributedText` to notify the view about the change 124 // when `text` native prop is set. 125 _previousAttributedText = nil; 126 [self dirtyLayout]; 127} 128 129#pragma mark - ABI47_0_0RCTUIManagerObserver 130 131- (void)uiManagerWillPerformMounting 132{ 133 if (ABI47_0_0YGNodeIsDirty(self.yogaNode)) { 134 return; 135 } 136 137 if (!_needsUpdateView) { 138 return; 139 } 140 _needsUpdateView = NO; 141 142 UIEdgeInsets borderInsets = self.borderAsInsets; 143 UIEdgeInsets paddingInsets = self.paddingAsInsets; 144 145 ABI47_0_0RCTTextAttributes *textAttributes = [self.textAttributes copy]; 146 147 NSMutableAttributedString *attributedText = 148 [[NSMutableAttributedString alloc] initWithAttributedString:[self attributedTextWithBaseTextAttributes:nil]]; 149 150 // Removing all references to Shadow Views and tags to avoid unnecessary retaining 151 // and problems with comparing the strings. 152 [attributedText removeAttribute:ABI47_0_0RCTBaseTextShadowViewEmbeddedShadowViewAttributeName 153 range:NSMakeRange(0, attributedText.length)]; 154 155 [attributedText removeAttribute:ABI47_0_0RCTTextAttributesTagAttributeName 156 range:NSMakeRange(0, attributedText.length)]; 157 158 if (self.text.length) { 159 NSAttributedString *propertyAttributedText = 160 [[NSAttributedString alloc] initWithString:self.text 161 attributes:self.textAttributes.effectiveTextAttributes]; 162 [attributedText insertAttributedString:propertyAttributedText atIndex:0]; 163 } 164 165 BOOL isAttributedTextChanged = NO; 166 if (![_previousAttributedText isEqualToAttributedString:attributedText]) { 167 // We have to follow `set prop` pattern: 168 // If the value has not changed, we must not notify the view about the change, 169 // otherwise we may break local (temporary) state of the text input. 170 isAttributedTextChanged = YES; 171 _previousAttributedText = [attributedText copy]; 172 } 173 174 NSNumber *tag = self.ABI47_0_0ReactTag; 175 176 [_bridge.uiManager addUIBlock:^(ABI47_0_0RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { 177 ABI47_0_0RCTBaseTextInputView *baseTextInputView = (ABI47_0_0RCTBaseTextInputView *)viewRegistry[tag]; 178 if (!baseTextInputView) { 179 return; 180 } 181 182 baseTextInputView.textAttributes = textAttributes; 183 baseTextInputView.ABI47_0_0ReactBorderInsets = borderInsets; 184 baseTextInputView.ABI47_0_0ReactPaddingInsets = paddingInsets; 185 186 if (isAttributedTextChanged) { 187 // Don't set `attributedText` if length equal to zero, otherwise it would shrink when attributes contain like `lineHeight`. 188 if (attributedText.length != 0) { 189 baseTextInputView.attributedText = attributedText; 190 } else { 191 baseTextInputView.attributedText = nil; 192 } 193 } 194 }]; 195} 196 197#pragma mark - 198 199- (NSAttributedString *)measurableAttributedText 200{ 201 // Only for the very first render when we don't have `_localAttributedText`, 202 // we use value directly from the property and/or nested content. 203 NSAttributedString *attributedText = 204 _localAttributedText ?: [self attributedTextWithBaseTextAttributes:nil]; 205 206 if (attributedText.length == 0) { 207 // It's impossible to measure empty attributed string because all attributes are 208 // associated with some characters, so no characters means no data. 209 210 // Placeholder also can represent the intrinsic size when it is visible. 211 NSString *text = self.placeholder; 212 if (!text.length) { 213 // Note: `zero-width space` is insufficient in some cases. 214 text = @"I"; 215 } 216 attributedText = [[NSAttributedString alloc] initWithString:text attributes:self.textAttributes.effectiveTextAttributes]; 217 } 218 219 return attributedText; 220} 221 222- (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize 223{ 224 NSAttributedString *attributedText = [self measurableAttributedText]; 225 226 if (!_textStorage) { 227 _textContainer = [NSTextContainer new]; 228 _textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. 229 _layoutManager = [NSLayoutManager new]; 230 [_layoutManager addTextContainer:_textContainer]; 231 _textStorage = [NSTextStorage new]; 232 [_textStorage addLayoutManager:_layoutManager]; 233 } 234 235 _textContainer.size = maximumSize; 236 _textContainer.maximumNumberOfLines = _maximumNumberOfLines; 237 [_textStorage replaceCharactersInRange:(NSRange){0, _textStorage.length} 238 withAttributedString:attributedText]; 239 [_layoutManager ensureLayoutForTextContainer:_textContainer]; 240 CGSize size = [_layoutManager usedRectForTextContainer:_textContainer].size; 241 242 return (CGSize){ 243 MAX(minimumSize.width, MIN(ABI47_0_0RCTCeilPixelValue(size.width), maximumSize.width)), 244 MAX(minimumSize.height, MIN(ABI47_0_0RCTCeilPixelValue(size.height), maximumSize.height)) 245 }; 246} 247 248- (CGFloat)lastBaselineForSize:(CGSize)size 249{ 250 NSAttributedString *attributedText = [self measurableAttributedText]; 251 252 __block CGFloat maximumDescender = 0.0; 253 254 [attributedText enumerateAttribute:NSFontAttributeName 255 inRange:NSMakeRange(0, attributedText.length) 256 options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired 257 usingBlock: 258 ^(UIFont *font, NSRange range, __unused BOOL *stop) { 259 if (maximumDescender > font.descender) { 260 maximumDescender = font.descender; 261 } 262 } 263 ]; 264 265 return size.height + maximumDescender; 266} 267 268static ABI47_0_0YGSize ABI47_0_0RCTBaseTextInputShadowViewMeasure(ABI47_0_0YGNodeRef node, float width, ABI47_0_0YGMeasureMode widthMode, float height, ABI47_0_0YGMeasureMode heightMode) 269{ 270 ABI47_0_0RCTShadowView *shadowView = (__bridge ABI47_0_0RCTShadowView *)ABI47_0_0YGNodeGetContext(node); 271 272 CGSize minimumSize = CGSizeMake(0, 0); 273 CGSize maximumSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); 274 275 CGSize size = { 276 ABI47_0_0RCTCoreGraphicsFloatFromYogaFloat(width), 277 ABI47_0_0RCTCoreGraphicsFloatFromYogaFloat(height) 278 }; 279 280 switch (widthMode) { 281 case ABI47_0_0YGMeasureModeUndefined: 282 break; 283 case ABI47_0_0YGMeasureModeExactly: 284 minimumSize.width = size.width; 285 maximumSize.width = size.width; 286 break; 287 case ABI47_0_0YGMeasureModeAtMost: 288 maximumSize.width = size.width; 289 break; 290 } 291 292 switch (heightMode) { 293 case ABI47_0_0YGMeasureModeUndefined: 294 break; 295 case ABI47_0_0YGMeasureModeExactly: 296 minimumSize.height = size.height; 297 maximumSize.height = size.height; 298 break; 299 case ABI47_0_0YGMeasureModeAtMost: 300 maximumSize.height = size.height; 301 break; 302 } 303 304 CGSize measuredSize = [shadowView sizeThatFitsMinimumSize:minimumSize maximumSize:maximumSize]; 305 306 return (ABI47_0_0YGSize){ 307 ABI47_0_0RCTYogaFloatFromCoreGraphicsFloat(measuredSize.width), 308 ABI47_0_0RCTYogaFloatFromCoreGraphicsFloat(measuredSize.height) 309 }; 310} 311 312static float ABI47_0_0RCTTextInputShadowViewBaseline(ABI47_0_0YGNodeRef node, const float width, const float height) 313{ 314 ABI47_0_0RCTBaseTextInputShadowView *shadowTextView = (__bridge ABI47_0_0RCTBaseTextInputShadowView *)ABI47_0_0YGNodeGetContext(node); 315 316 CGSize size = (CGSize){ 317 ABI47_0_0RCTCoreGraphicsFloatFromYogaFloat(width), 318 ABI47_0_0RCTCoreGraphicsFloatFromYogaFloat(height) 319 }; 320 321 CGFloat lastBaseline = [shadowTextView lastBaselineForSize:size]; 322 323 return ABI47_0_0RCTYogaFloatFromCoreGraphicsFloat(lastBaseline); 324} 325 326@end 327