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