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