From 0b9ab6ad46eb659a0ab23e4b375d006e4a5ea4d8 Mon Sep 17 00:00:00 2001 From: Nick Lefever Date: Wed, 20 Sep 2023 18:25:38 +0200 Subject: [PATCH 1/3] [fabric] Add text storage getter to set up text views outside text layout manager Summary: To render text using an NSTextView, we need to gain access to a fully configured NSTextStorage to configure the text view to render the text with the right configuration. Test Plan: Tested later in this stack. Reviewers: shawndempsey, chpurrer, #rn-desktop Reviewed By: shawndempsey Differential Revision: https://phabricator.intern.facebook.com/D49465173 Tasks: T163838519 --- .../textlayoutmanager/RCTTextLayoutManager.h | 6 ++++++ .../textlayoutmanager/RCTTextLayoutManager.mm | 14 ++++++++++++++ 2 files changed, 20 insertions(+) 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 From d1852ed93c302ae4a750db47883b0081ee9e29a2 Mon Sep 17 00:00:00 2001 From: Nick Lefever Date: Wed, 20 Sep 2023 18:42:18 +0200 Subject: [PATCH 2/3] [fabric] Render Paragraph text using NSTextView Summary: Use an NSTextView to render the text in RCTParagraphComponentView so that we would get UX interactions specific to desktop for free (e.g. text selection) Test Plan: - Run Zeratul with Fabric enabled - Check that text is being rendered correctly with the right size and positioning. {F1097505779} Reviewers: shawndempsey, chpurrer, #rn-desktop Reviewed By: shawndempsey Differential Revision: https://phabricator.intern.facebook.com/D49465175 Tasks: T163838519 --- .../Text/RCTParagraphComponentView.mm | 106 +++++++++++++++++- 1 file changed, 103 insertions(+), 3 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 19ba5d2084416e..152cde6f40a29a 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -29,6 +29,7 @@ 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] @@ -38,6 +39,14 @@ @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 () @@ -53,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 @@ -64,10 +75,29 @@ - (instancetype)initWithFrame:(CGRect)frame #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; @@ -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]; @@ -132,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 @@ -143,10 +178,53 @@ - (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 { @@ -349,6 +427,7 @@ - (void)copy:(id)sender return RCTParagraphComponentView.class; } +#if !TARGET_OS_OSX // [macOS] @implementation RCTParagraphTextView { CAShapeLayer *_highlightLayer; } @@ -390,3 +469,24 @@ - (void)drawRect:(CGRect)rect } @end +#else // [macOS +#if TARGET_OS_OSX // [macOS +@implementation RCTParagraphComponentUnfocusableTextView + +- (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]; +} + +@end +#endif // macOS] From a06142f23df188923a1ffbf46518a43195a0c897 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 15 Nov 2024 14:11:05 -0600 Subject: [PATCH 3/3] chore(fabric, text): Refactor macOS implementation to use contentView --- .../Text/RCTParagraphComponentView.mm | 106 +++++++----------- 1 file changed, 42 insertions(+), 64 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 152cde6f40a29a..b5e5f3c5c590ab 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm @@ -33,20 +33,22 @@ // 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 () @@ -62,10 +64,8 @@ @implementation RCTParagraphComponentView { RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; #if !TARGET_OS_OSX // [macOS] UILongPressGestureRecognizer *_longPressGestureRecognizer; - RCTParagraphTextView *_textView; -#else // [macOS - RCTParagraphComponentUnfocusableTextView *_textView; #endif // macOS] + RCTParagraphTextView *_textView; } - (instancetype)initWithFrame:(CGRect)frame @@ -77,7 +77,6 @@ - (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; @@ -85,7 +84,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 = [[RCTParagraphComponentUnfocusableTextView alloc] initWithFrame:self.bounds]; + _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; @@ -98,6 +97,7 @@ - (instancetype)initWithFrame:(CGRect)frame self.contentView = _textView; self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; #endif // macOS] + self.contentView = _textView; } return self; @@ -164,12 +164,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,53 +175,10 @@ - (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 { @@ -427,11 +381,13 @@ - (void)copy:(id)sender return RCTParagraphComponentView.class; } -#if !TARGET_OS_OSX // [macOS] @implementation RCTParagraphTextView { +#if !TARGET_OS_OSX // [macOS] CAShapeLayer *_highlightLayer; +#endif // macOS] } + - (void)drawRect:(CGRect)rect { if (!_state) { @@ -449,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 @@ -466,12 +423,33 @@ - (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 -@implementation RCTParagraphComponentUnfocusableTextView +- (void)setNeedsDisplay +{ + [self setNeedsDisplay:YES]; +} - (BOOL)canBecomeKeyView { @@ -487,6 +465,6 @@ - (BOOL)resignFirstResponder return [super resignFirstResponder]; } +#endif // macOS] @end -#endif // macOS]