RCTBaseTextInputShadowView.m 9.5 KB

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