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