diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm index 19ba5d2084416e..b5e5f3c5c590ab 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -29,14 +29,25 @@ using namespace facebook::react; +#if !TARGET_OS_OSX // [macOS] // ParagraphTextView is an auxiliary view we set as contentView so the drawing // can happen on top of the layers manipulated by RCTViewComponentView (the parent view) @interface RCTParagraphTextView : RCTUIView // [macOS] +#else // [macOS +// On macOS, we also defer drawing to an NSTextView, +// in order to get more native behaviors like text selection. +@interface RCTParagraphTextView : NSTextView // [macOS] +#endif // macOS] @property (nonatomic) ParagraphShadowNode::ConcreteState::Shared state; @property (nonatomic) ParagraphAttributes paragraphAttributes; @property (nonatomic) LayoutMetrics layoutMetrics; +#if TARGET_OS_OSX // [macOS] +/// UIKit compatibility shim that simply calls `[self setNeedsDisplay:YES]` +- (void)setNeedsDisplay; +#endif + @end #if !TARGET_OS_OSX // [macOS] @@ -53,7 +64,7 @@ @implementation RCTParagraphComponentView { RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; #if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; -#endif // [macOS] +#endif // macOS] RCTParagraphTextView *_textView; } @@ -64,9 +75,28 @@ - (instancetype)initWithFrame:(CGRect)frame #if !TARGET_OS_OSX // [macOS] self.opaque = NO; -#endif // [macOS] _textView = [RCTParagraphTextView new]; _textView.backgroundColor = RCTUIColor.clearColor; // [macOS] +#else // [macOS + // Make the RCTParagraphComponentView accessible and available in the a11y hierarchy. + self.accessibilityElement = YES; + self.accessibilityRole = NSAccessibilityStaticTextRole; + // Fix blurry text on non-retina displays. + self.canDrawSubviewsIntoLayer = YES; + // The NSTextView is responsible for drawing text and managing selection. + _textView = [[RCTParagraphTextView alloc] initWithFrame:self.bounds]; + // The RCTParagraphComponentUnfocusableTextView is only used for rendering and should not appear in the a11y hierarchy. + _textView.accessibilityElement = NO; + _textView.usesFontPanel = NO; + _textView.drawsBackground = NO; + _textView.linkTextAttributes = @{}; + _textView.editable = NO; + _textView.selectable = NO; + _textView.verticallyResizable = NO; + _textView.layoutManager.usesFontLeading = NO; + self.contentView = _textView; + self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; +#endif // macOS] self.contentView = _textView; } @@ -123,7 +153,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } else { [self disableContextMenu]; } -#endif // [macOS] +#else // [macOS + _textView.selectable = newParagraphProps.isSelectable; +#endif // macOS] } [super updateProps:props oldProps:oldProps]; @@ -350,9 +382,12 @@ - (void)copy:(id)sender } @implementation RCTParagraphTextView { +#if !TARGET_OS_OSX // [macOS] CAShapeLayer *_highlightLayer; +#endif // macOS] } + - (void)drawRect:(CGRect)rect { if (!_state) { @@ -370,6 +405,7 @@ - (void)drawRect:(CGRect)rect CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); +#if !TARGET_OS_OSX // [macOS] [nativeTextLayoutManager drawAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame @@ -387,6 +423,48 @@ - (void)drawRect:(CGRect)rect self->_highlightLayer = nil; } }]; +#else // [macOS + NSTextStorage *textStorage = [nativeTextLayoutManager getTextStorageForAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes size:frame.size]; + + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + + [self replaceTextContainer:textContainer]; + + NSArray *managers = [[textStorage layoutManagers] copy]; + for (NSLayoutManager *manager in managers) { + [textStorage removeLayoutManager:manager]; + } + + self.minSize = frame.size; + self.maxSize = frame.size; + self.frame = frame; + [[self textStorage] setAttributedString:textStorage]; + + [super drawRect:rect]; +#endif +} + +#if TARGET_OS_OSX // [macOS +- (void)setNeedsDisplay +{ + [self setNeedsDisplay:YES]; +} + +- (BOOL)canBecomeKeyView +{ + return NO; } +- (BOOL)resignFirstResponder +{ + // Don't relinquish first responder while selecting text. + if (self.selectable && NSRunLoop.currentRunLoop.currentMode == NSEventTrackingRunLoopMode) { + return NO; + } + + return [super resignFirstResponder]; +} +#endif // macOS] + @end diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h index 4c7c431fb2a5a1..672090101c8a7e 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h @@ -56,6 +56,12 @@ using RCTTextLayoutFragmentEnumerationBlock = frame:(CGRect)frame usingBlock:(RCTTextLayoutFragmentEnumerationBlock)block; +#if TARGET_OS_OSX // [macOS +- (NSTextStorage *)getTextStorageForAttributedString:(facebook::react::AttributedString)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + size:(CGSize)size; +#endif // macOS] + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index e63cc78b49382a..390a07c8f6ead2 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -399,4 +399,18 @@ - (TextMeasurement)_measureTextStorage:(NSTextStorage *)textStorage return TextMeasurement{{size.width, size.height}, attachments}; } +#if TARGET_OS_OSX // [macOS +- (NSTextStorage *)getTextStorageForAttributedString:(AttributedString)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + size:(CGSize)size +{ + NSAttributedString *nsAttributedString = [self _nsAttributedStringFromAttributedString:attributedString]; + NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:nsAttributedString + paragraphAttributes:paragraphAttributes + size:size]; + + return textStorage; +} +#endif // macOS] + @end