1#import "T3SourceText.h"
2#import <React/RCTFabricComponentsPlugins.h>
3#import "T3SourceTextComponentDescriptor.h"
4#import "T3SourceTextShadowNode.h"
5#import <React/RCTConversions.h>
6#import <objc/runtime.h>
7#import <react/renderer/components/T3SourceTextSpec/EventEmitters.h>
8#import <react/renderer/components/T3SourceTextSpec/Props.h>
9#import <react/renderer/components/T3SourceTextSpec/RCTComponentViewHelpers.h>
10#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
11
12using namespace facebook::react;
13
14@interface T3SourceTextView : UITextView
15@end
16
17@implementation T3SourceTextView
18- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
19 // Read-only UITextViews must still allow selecting the whole file.
20 if (action == @selector(selectAll:)) {
21 return self.selectable && self.text.length > 0 &&
22 self.selectedRange.length < self.text.length;
23 }
24 return [super canPerformAction:action withSender:sender];
25}
26@end
27
28@protocol T3SourceTextOutsideTapTarget <NSObject>
29- (void)clearSelectionForOutsideTapWithHitView:(UIView *)hitView;
30@end
31
32@interface T3SourceTextOutsideTapCoordinator
33 : NSObject <UIGestureRecognizerDelegate>
34
35- (instancetype)initWithWindow:(UIWindow *)window;
36- (void)addTarget:(id<T3SourceTextOutsideTapTarget>)target;
37- (void)removeTarget:(id<T3SourceTextOutsideTapTarget>)target;
38
39@end
40
41static const void *T3SourceTextOutsideTapCoordinatorKey =
42 &T3SourceTextOutsideTapCoordinatorKey;
43
44@implementation T3SourceTextOutsideTapCoordinator {
45 __weak UIWindow *_window;
46 UITapGestureRecognizer *_recognizer;
47 NSHashTable<id<T3SourceTextOutsideTapTarget>> *_targets;
48}
49
50- (instancetype)initWithWindow:(UIWindow *)window {
51 if (self = [super init]) {
52 _window = window;
53 _targets = [NSHashTable weakObjectsHashTable];
54 _recognizer =
55 [[UITapGestureRecognizer alloc] initWithTarget:self
56 action:@selector(handleTap:)];
57 _recognizer.cancelsTouchesInView = NO;
58 _recognizer.delegate = self;
59 [window addGestureRecognizer:_recognizer];
60 }
61 return self;
62}
63
64- (void)addTarget:(id<T3SourceTextOutsideTapTarget>)target {
65 [_targets addObject:target];
66}
67
68- (void)removeTarget:(id<T3SourceTextOutsideTapTarget>)target {
69 [_targets removeObject:target];
70 if (_targets.count > 0) {
71 return;
72 }
73
74 UIWindow *window = _window;
75 [window removeGestureRecognizer:_recognizer];
76 if (objc_getAssociatedObject(window, T3SourceTextOutsideTapCoordinatorKey) ==
77 self) {
78 objc_setAssociatedObject(window, T3SourceTextOutsideTapCoordinatorKey, nil,
79 OBJC_ASSOCIATION_RETAIN_NONATOMIC);
80 }
81}
82
83- (void)handleTap:(UITapGestureRecognizer *)sender {
84 UIWindow *window = _window;
85 if (window == nil) {
86 return;
87 }
88
89 UIView *hitView = [window hitTest:[sender locationInView:window]
90 withEvent:nil];
91 if (hitView == nil) {
92 return;
93 }
94 for (id<T3SourceTextOutsideTapTarget> target in _targets.allObjects) {
95 [target clearSelectionForOutsideTapWithHitView:hitView];
96 }
97}
98
99- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
100 shouldRecognizeSimultaneouslyWithGestureRecognizer:
101 (UIGestureRecognizer *)otherGestureRecognizer {
102 return YES;
103}
104
105@end
106
107static T3SourceTextOutsideTapCoordinator *
108T3SourceTextOutsideTapCoordinatorForWindow(UIWindow *window) {
109 T3SourceTextOutsideTapCoordinator *coordinator =
110 objc_getAssociatedObject(window, T3SourceTextOutsideTapCoordinatorKey);
111 if (coordinator == nil) {
112 coordinator =
113 [[T3SourceTextOutsideTapCoordinator alloc] initWithWindow:window];
114 objc_setAssociatedObject(window, T3SourceTextOutsideTapCoordinatorKey,
115 coordinator, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
116 }
117 return coordinator;
118}
119
120@interface T3SourceText () <RCTT3SourceTextViewProtocol, UITextViewDelegate,
121 T3SourceTextOutsideTapTarget>
122@end
123
124@implementation T3SourceText {
125 UIView *_view;
126 T3SourceTextView *_textView;
127 BOOL _textLayoutNeedsUpdate;
128 T3SourceTextShadowNode::ConcreteState::Shared _state;
129 __weak UIWindow *_outsideTapWindow;
130 BOOL _suppressSelectionChange;
131}
132
133+ (ComponentDescriptorProvider)componentDescriptorProvider {
134 return concreteComponentDescriptorProvider<T3SourceTextComponentDescriptor>();
135}
136
137- (instancetype)initWithFrame:(CGRect)frame {
138 if (self = [super initWithFrame:frame]) {
139 static const auto defaultProps =
140 std::make_shared<const T3SourceTextProps>();
141 _props = defaultProps;
142 _view = [[UIView alloc] init];
143 self.contentView = _view;
144 self.clipsToBounds = YES;
145 _textView = [[T3SourceTextView alloc] init];
146 _textView.scrollEnabled = NO;
147 _textView.editable = NO;
148 _textView.textContainerInset = UIEdgeInsetsZero;
149 _textView.textContainer.lineFragmentPadding = 0;
150 _textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
151 _textView.textContainer.widthTracksTextView = NO;
152 _textView.textContainer.heightTracksTextView = NO;
153 _textView.delegate = self;
154 _textView.textDragInteraction.enabled = NO;
155 _textView.layoutManager.usesFontLeading = NO;
156 [self addSubview:_textView];
157 }
158 return self;
159}
160
161- (void)didMoveToWindow {
162 [super didMoveToWindow];
163 if (_outsideTapWindow == self.window)
164 return;
165 T3SourceTextOutsideTapCoordinator *coordinator = objc_getAssociatedObject(
166 _outsideTapWindow, T3SourceTextOutsideTapCoordinatorKey);
167 [coordinator removeTarget:self];
168 _outsideTapWindow = self.window;
169 if (_outsideTapWindow != nil) {
170 [T3SourceTextOutsideTapCoordinatorForWindow(_outsideTapWindow)
171 addTarget:self];
172 }
173}
174
175- (void)dealloc {
176 T3SourceTextOutsideTapCoordinator *coordinator = objc_getAssociatedObject(
177 _outsideTapWindow, T3SourceTextOutsideTapCoordinatorKey);
178 [coordinator removeTarget:self];
179}
180
181- (void)prepareForRecycle {
182 [super prepareForRecycle];
183 T3SourceTextOutsideTapCoordinator *coordinator = objc_getAssociatedObject(
184 _outsideTapWindow, T3SourceTextOutsideTapCoordinatorKey);
185 [coordinator removeTarget:self];
186 _outsideTapWindow = nil;
187 _state.reset();
188 _textView.frame = CGRectZero;
189 _textView.attributedText = nil;
190 _textLayoutNeedsUpdate = YES;
191}
192
193- (void)layoutSubviews {
194 [super layoutSubviews];
195 if (!CGRectEqualToRect(_textView.frame, _view.frame)) {
196 _textView.frame = _view.frame;
197 _textLayoutNeedsUpdate = YES;
198 [self setNeedsDisplay];
199 }
200}
201
202- (void)drawRect:(CGRect)rect {
203 if (!_state)
204 return;
205 NSMutableAttributedString *text = [RCTNSAttributedStringFromAttributedString(
206 _state->getData().attributedString) mutableCopy];
207 RCTApplyBaselineOffset(text);
208 const BOOL textChanged =
209 ![_textView.attributedText isEqualToAttributedString:text];
210 const BOOL frameChanged = _textLayoutNeedsUpdate;
211 if (!textChanged && !frameChanged)
212 return;
213 if (textChanged) {
214 const NSRange savedRange = _textView.selectedRange;
215 _suppressSelectionChange = YES;
216 _textView.attributedText = text;
217 if (savedRange.location != NSNotFound && savedRange.length > 0 &&
218 NSMaxRange(savedRange) <= text.length) {
219 _textView.selectedRange = savedRange;
220 }
221 _suppressSelectionChange = NO;
222 }
223 if (frameChanged)
224 _textView.textContainer.size = CGSizeMake(CGRectGetWidth(_view.frame), CGFLOAT_MAX);
225 _textLayoutNeedsUpdate = NO;
226}
227
228- (void)updateProps:(Props::Shared const &)props
229 oldProps:(Props::Shared const &)oldProps {
230 const auto &oldViewProps =
231 *std::static_pointer_cast<T3SourceTextProps const>(_props);
232 const auto &newViewProps =
233 *std::static_pointer_cast<T3SourceTextProps const>(props);
234 if (oldViewProps.numberOfLines != newViewProps.numberOfLines) {
235 _textView.textContainer.maximumNumberOfLines = newViewProps.numberOfLines;
236 }
237 if (oldViewProps.selectable != newViewProps.selectable)
238 _textView.selectable = newViewProps.selectable;
239 if (oldViewProps.allowFontScaling != newViewProps.allowFontScaling) {
240 _textView.adjustsFontForContentSizeCategory = newViewProps.allowFontScaling;
241 }
242 if (oldViewProps.ellipsizeMode != newViewProps.ellipsizeMode) {
243 switch (newViewProps.ellipsizeMode) {
244 case T3SourceTextEllipsizeMode::Head:
245 _textView.textContainer.lineBreakMode = NSLineBreakByTruncatingHead;
246 break;
247 case T3SourceTextEllipsizeMode::Middle:
248 _textView.textContainer.lineBreakMode = NSLineBreakByTruncatingMiddle;
249 break;
250 case T3SourceTextEllipsizeMode::Tail:
251 _textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
252 break;
253 case T3SourceTextEllipsizeMode::Clip:
254 _textView.textContainer.lineBreakMode = NSLineBreakByClipping;
255 break;
256 }
257 }
258 if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
259 _textView.backgroundColor =
260 RCTUIColorFromSharedColor(newViewProps.backgroundColor);
261 }
262 [super updateProps:props oldProps:oldProps];
263}
264
265- (void)updateState:(const facebook::react::State::Shared &)state
266 oldState:(const facebook::react::State::Shared &)oldState {
267 _state =
268 std::static_pointer_cast<const T3SourceTextShadowNode::ConcreteState>(
269 state);
270 [self setNeedsDisplay];
271}
272
273- (void)clearSelectionForOutsideTapWithHitView:(UIView *)hitView {
274 if ([hitView isDescendantOfView:self])
275 return;
276 UITextView *textView = _textView;
277 // Allow a pending native Copy action to consume its selection first.
278 dispatch_async(dispatch_get_main_queue(), ^{
279 UITextRange *range = textView.selectedTextRange;
280 if (range != nil && !range.isEmpty)
281 textView.selectedTextRange = nil;
282 });
283}
284
285- (void)textViewDidChangeSelection:(UITextView *)textView {
286 if (_suppressSelectionChange || _eventEmitter == nullptr)
287 return;
288 const NSRange range = textView.selectedRange;
289 if (range.location == NSNotFound)
290 return;
291 std::dynamic_pointer_cast<const facebook::react::T3SourceTextEventEmitter>(
292 _eventEmitter)
293 ->onSelectionChange(
294 facebook::react::T3SourceTextEventEmitter::OnSelectionChange{
295 static_cast<int>(self.tag), static_cast<int>(range.location),
296 static_cast<int>(NSMaxRange(range))});
297}
298
299Class<RCTComponentViewProtocol> T3SourceTextCls(void) {
300 return T3SourceText.class;
301}
302@end