From 34fcdcab8089d71a6bf32c3974d0ebfc4c747ae2 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 5 Sep 2025 15:50:49 -0700 Subject: [PATCH 1/2] Revert "chore(fabric, text): Refactor macOS implementation to use contentView" This reverts commit 22b706348215c1e1037ae80cb560bffc3657643f. --- .../Text/RCTParagraphComponentView.mm | 105 +++++++++++------- 1 file changed, 64 insertions(+), 41 deletions(-) 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 6e3284b91c728e..19e060b517c69e 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -33,22 +33,20 @@ // 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 +#else // [macOS +#if TARGET_OS_OSX // [macOS +// On macOS, we defer drawing to an NSTextView rather than a plan NSView, in order +// to get more native behaviors like text selection. We make sure this NSTextView +// does not take focus. +@interface RCTParagraphComponentUnfocusableTextView : NSTextView +@end +#endif // macOS] #if !TARGET_OS_OSX // [macOS] @interface RCTParagraphComponentView () @@ -64,8 +62,10 @@ @implementation RCTParagraphComponentView { RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; #if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; -#endif // macOS] RCTParagraphTextView *_textView; +#else // [macOS + RCTParagraphComponentUnfocusableTextView *_textView; +#endif // macOS] } - (instancetype)initWithFrame:(CGRect)frame @@ -77,6 +77,7 @@ - (instancetype)initWithFrame:(CGRect)frame self.opaque = NO; _textView = [RCTParagraphTextView new]; _textView.backgroundColor = RCTUIColor.clearColor; // [macOS] + self.contentView = _textView; #else // [macOS // Make the RCTParagraphComponentView accessible and available in the a11y hierarchy. self.accessibilityElement = YES; @@ -84,7 +85,7 @@ - (instancetype)initWithFrame:(CGRect)frame // 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]; + _textView = [[RCTParagraphComponentUnfocusableTextView 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; @@ -97,7 +98,6 @@ - (instancetype)initWithFrame:(CGRect)frame self.contentView = _textView; self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; #endif // macOS] - self.contentView = _textView; } return self; @@ -164,9 +164,12 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState { _state = std::static_pointer_cast(state); +#if !TARGET_OS_OSX // [macOS] _textView.state = _state; [_textView setNeedsDisplay]; [self setNeedsLayout]; + [self _updateTextView]; +#endif // macOS] } - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics @@ -175,11 +178,54 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics // Using stored `_layoutMetrics` as `oldLayoutMetrics` here to avoid // re-applying individual sub-values which weren't changed. [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics]; +#if !TARGET_OS_OSX // [macOS] _textView.layoutMetrics = _layoutMetrics; [_textView setNeedsDisplay]; [self setNeedsLayout]; +#else // [macOS + [self _updateTextView]; +#endif // macOS] } +#if TARGET_OS_OSX // [macOS +- (void)_updateTextView +{ + if (!_state) { + return; + } + + auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager(); + + if (!textLayoutManager) { + return; + } + + RCTTextLayoutManager *nativeTextLayoutManager = + (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); + + CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); + + NSTextStorage *textStorage = [nativeTextLayoutManager getTextStorageForAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame]; + + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; + + [_textView replaceTextContainer:textContainer]; + + NSArray *managers = [[textStorage layoutManagers] copy]; + for (NSLayoutManager *manager in managers) { + [textStorage removeLayoutManager:manager]; + } + + _textView.minSize = frame.size; + _textView.maxSize = frame.size; + _textView.frame = frame; + _textView.textStorage.attributedString = textStorage; + + [self setNeedsDisplay]; +} +#endif // macOS] + - (void)prepareForRecycle { [super prepareForRecycle]; @@ -381,10 +427,9 @@ - (void)copy:(id)sender return RCTParagraphComponentView.class; } -@implementation RCTParagraphTextView { #if !TARGET_OS_OSX // [macOS] +@implementation RCTParagraphTextView { CAShapeLayer *_highlightLayer; -#endif // macOS] } - (RCTUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS] @@ -409,7 +454,6 @@ - (void)drawRect:(CGRect)rect CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); -#if !TARGET_OS_OSX // [macOS] [nativeTextLayoutManager drawAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame @@ -427,33 +471,12 @@ - (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 } +@end +#else // [macOS #if TARGET_OS_OSX // [macOS -- (void)setNeedsDisplay -{ - [self setNeedsDisplay:YES]; -} +@implementation RCTParagraphComponentUnfocusableTextView - (BOOL)canBecomeKeyView { @@ -469,6 +492,6 @@ - (BOOL)resignFirstResponder return [super resignFirstResponder]; } -#endif // macOS] @end +#endif // macOS] From 725aca329fc9b47c5f0f1745f59af05f9a42293a Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 9 Sep 2025 17:02:04 -0700 Subject: [PATCH 2/2] redo from scratch --- .../Text/RCTParagraphComponentView.mm | 181 +++++++----------- 1 file changed, 73 insertions(+), 108 deletions(-) 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 19e060b517c69e..637b3917cf6d68 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -11,6 +11,9 @@ #if !TARGET_OS_OSX // [macOS] #import #endif // [macOS] +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] #import #import @@ -25,11 +28,8 @@ #import "RCTConversions.h" #import "RCTFabricComponentsPlugins.h" -#import // [macOS] - 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] @@ -39,20 +39,15 @@ @interface RCTParagraphTextView : RCTUIView // [macOS] @property (nonatomic) LayoutMetrics layoutMetrics; @end -#else // [macOS -#if TARGET_OS_OSX // [macOS -// On macOS, we defer drawing to an NSTextView rather than a plan NSView, in order -// to get more native behaviors like text selection. We make sure this NSTextView -// does not take focus. -@interface RCTParagraphComponentUnfocusableTextView : NSTextView -@end -#endif // macOS] #if !TARGET_OS_OSX // [macOS] @interface RCTParagraphComponentView () @property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0)); +@end +#else // [macOS +@interface RCTParagraphComponentView () @end #endif // [macOS] @@ -62,10 +57,8 @@ @implementation RCTParagraphComponentView { RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; #if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; +#endif // [macOS] RCTParagraphTextView *_textView; -#else // [macOS - RCTParagraphComponentUnfocusableTextView *_textView; -#endif // macOS] } - (instancetype)initWithFrame:(CGRect)frame @@ -73,31 +66,12 @@ - (instancetype)initWithFrame:(CGRect)frame if (self = [super initWithFrame:frame]) { _props = ParagraphShadowNode::defaultSharedProps(); -#if !TARGET_OS_OSX // [macOS] +#if !TARGET_OS_OSX // [macOS] self.opaque = NO; +#endif // [macOS] _textView = [RCTParagraphTextView new]; _textView.backgroundColor = RCTUIColor.clearColor; // [macOS] self.contentView = _textView; -#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 = [[RCTParagraphComponentUnfocusableTextView 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] } return self; @@ -153,9 +127,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & } else { [self disableContextMenu]; } -#else // [macOS - _textView.selectable = newParagraphProps.isSelectable; -#endif // macOS] +#endif // [macOS] } [super updateProps:props oldProps:oldProps]; @@ -164,12 +136,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & - (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState { _state = std::static_pointer_cast(state); -#if !TARGET_OS_OSX // [macOS] _textView.state = _state; [_textView setNeedsDisplay]; [self setNeedsLayout]; - [self _updateTextView]; -#endif // macOS] } - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics @@ -178,54 +147,11 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics // Using stored `_layoutMetrics` as `oldLayoutMetrics` here to avoid // re-applying individual sub-values which weren't changed. [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics]; -#if !TARGET_OS_OSX // [macOS] _textView.layoutMetrics = _layoutMetrics; [_textView setNeedsDisplay]; [self setNeedsLayout]; -#else // [macOS - [self _updateTextView]; -#endif // macOS] } -#if TARGET_OS_OSX // [macOS -- (void)_updateTextView -{ - if (!_state) { - return; - } - - auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager(); - - if (!textLayoutManager) { - return; - } - - RCTTextLayoutManager *nativeTextLayoutManager = - (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); - - CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); - - NSTextStorage *textStorage = [nativeTextLayoutManager getTextStorageForAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame]; - - NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; - NSTextContainer *textContainer = layoutManager.textContainers.firstObject; - - [_textView replaceTextContainer:textContainer]; - - NSArray *managers = [[textStorage layoutManagers] copy]; - for (NSLayoutManager *manager in managers) { - [textStorage removeLayoutManager:manager]; - } - - _textView.minSize = frame.size; - _textView.maxSize = frame.size; - _textView.frame = frame; - _textView.textStorage.attributedString = textStorage; - - [self setNeedsDisplay]; -} -#endif // macOS] - - (void)prepareForRecycle { [super prepareForRecycle]; @@ -259,7 +185,7 @@ - (BOOL)isAccessibilityElement return NO; } -#if !TARGET_OS_OSX // [macOS] +#if !TARGET_OS_OSX // [macOS - (NSArray *)accessibilityElements { const auto ¶graphProps = static_cast(*_props); @@ -295,7 +221,12 @@ - (UIAccessibilityTraits)accessibilityTraits { return [super accessibilityTraits] | UIAccessibilityTraitStaticText; } -#endif // [macOS] +#else // [macOS +- (NSAccessibilityRole)accessibilityRole +{ + return [super accessibilityRole] ?: NSAccessibilityStaticTextRole; +} +#endif // macOS] #pragma mark - RCTTouchableComponentViewProtocol @@ -379,6 +310,7 @@ - (BOOL)canBecomeFirstResponder return paragraphProps.isSelectable; } +#if !TARGET_OS_OSX // [macOS] - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { const auto ¶graphProps = static_cast(*_props); @@ -387,12 +319,9 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return YES; } -#if !TARGET_OS_OSX // [macOS] return [self.nextResponder canPerformAction:action withSender:sender]; -#else // [macOS - return NO; -#endif // macOS] } +#endif // [macOS] - (void)copy:(id)sender { @@ -427,12 +356,11 @@ - (void)copy:(id)sender return RCTParagraphComponentView.class; } -#if !TARGET_OS_OSX // [macOS] @implementation RCTParagraphTextView { CAShapeLayer *_highlightLayer; } -- (RCTUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS] +- (RCTUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { return nil; } @@ -465,7 +393,12 @@ - (void)drawRect:(CGRect)rect [self.layer addSublayer:self->_highlightLayer]; } self->_highlightLayer.position = frame.origin; + +#if !TARGET_OS_OSX // [macOS] self->_highlightLayer.path = highlightPath.CGPath; +#else // [macOS Update once our minimum is macOS 14 + self->_highlightLayer.path = UIBezierPathCreateCGPathRef(highlightPath); +#endif // macOS] } else { [self->_highlightLayer removeFromSuperlayer]; self->_highlightLayer = nil; @@ -474,24 +407,56 @@ - (void)drawRect:(CGRect)rect } @end -#else // [macOS -#if TARGET_OS_OSX // [macOS -@implementation RCTParagraphComponentUnfocusableTextView -- (BOOL)canBecomeKeyView -{ - return NO; -} - -- (BOOL)resignFirstResponder +#if TARGET_OS_OSX // [macOS +// Copied from RCTUIKit +CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *bezierPath) { - // Don't relinquish first responder while selecting text. - if (self.selectable && NSRunLoop.currentRunLoop.currentMode == NSEventTrackingRunLoopMode) { - return NO; + CGPathRef immutablePath = NULL; + + // Draw the path elements. + NSInteger numElements = [bezierPath elementCount]; + if (numElements > 0) + { + CGMutablePathRef path = CGPathCreateMutable(); + NSPoint points[3]; + BOOL didClosePath = YES; + + for (NSInteger i = 0; i < numElements; i++) + { + switch ([bezierPath elementAtIndex:i associatedPoints:points]) + { + case NSBezierPathElementMoveTo: + CGPathMoveToPoint(path, NULL, points[0].x, points[0].y); + break; + + case NSBezierPathElementLineTo: + CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); + didClosePath = NO; + break; + + case NSBezierPathElementCurveTo: + CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, + points[1].x, points[1].y, + points[2].x, points[2].y); + didClosePath = NO; + break; + + case NSBezierPathElementClosePath: + CGPathCloseSubpath(path); + didClosePath = YES; + break; + } + } + + // Be sure the path is closed or Quartz may not do valid hit detection. + if (!didClosePath) + CGPathCloseSubpath(path); + + immutablePath = CGPathCreateCopy(path); + CGPathRelease(path); } - - return [super resignFirstResponder]; + + return immutablePath; } - -@end #endif // macOS]