Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@

#if !TARGET_OS_OSX // [macOS]
#import <MobileCoreServices/UTCoreTypes.h>
#endif // [macOS]
#if TARGET_OS_OSX // [macOS
#import <QuartzCore/CAShapeLayer.h>
#else // [macOS
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't want else here

#import <React/RCTSurfaceTouchHandler.h>
#endif // macOS]

#import <react/renderer/components/text/ParagraphComponentDescriptor.h>
Expand All @@ -28,16 +27,29 @@
#import "RCTConversions.h"
#import "RCTFabricComponentsPlugins.h"

#import <QuartzCore/QuartzCore.h> // [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]
#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 // macOS]

@end

#if !TARGET_OS_OSX // [macOS]
Expand All @@ -47,7 +59,7 @@ @interface RCTParagraphComponentView () <UIEditMenuInteractionDelegate>

@end
#else // [macOS
@interface RCTParagraphComponentView ()
@interface RCTParagraphComponentView () <NSTextViewDelegate>
@end
#endif // [macOS]

Expand All @@ -57,7 +69,7 @@ @implementation RCTParagraphComponentView {
RCTParagraphComponentAccessibilityProvider *_accessibilityProvider;
#if !TARGET_OS_OSX // [macOS]
UILongPressGestureRecognizer *_longPressGestureRecognizer;
#endif // [macOS]
#endif // macOS]
RCTParagraphTextView *_textView;
}

Expand All @@ -66,11 +78,29 @@ - (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]
#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;
_textView = [[RCTParagraphTextView alloc] initWithFrame:self.bounds];
_textView.delegate = self;
_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;
}

Expand Down Expand Up @@ -127,7 +157,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];
Expand Down Expand Up @@ -185,7 +217,7 @@ - (BOOL)isAccessibilityElement
return NO;
}

#if !TARGET_OS_OSX // [macOS
#if !TARGET_OS_OSX // [macOS]
- (NSArray *)accessibilityElements
{
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
Expand Down Expand Up @@ -221,12 +253,7 @@ - (UIAccessibilityTraits)accessibilityTraits
{
return [super accessibilityTraits] | UIAccessibilityTraitStaticText;
}
#else // [macOS
- (NSAccessibilityRole)accessibilityRole
{
return [super accessibilityRole] ?: NSAccessibilityStaticTextRole;
}
#endif // macOS]
#endif // [macOS]

#pragma mark - RCTTouchableComponentViewProtocol

Expand Down Expand Up @@ -302,13 +329,87 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
[menuController showMenuFromView:self rect:self.bounds];
}
}
#endif // [macOS]
#else // [macOS
- (NSView *)hitTest:(NSPoint)point
{
// We will forward mouse click events to the NSTextView ourselves to prevent NSTextView from swallowing events that may be handled in JS (e.g. long press).
NSView *hitView = [super hitTest:point];

NSEventType eventType = NSApp.currentEvent.type;
BOOL isMouseClickEvent = NSEvent.pressedMouseButtons > 0;
BOOL isMouseMoveEventType = eventType == NSEventTypeMouseMoved || eventType == NSEventTypeMouseEntered || eventType == NSEventTypeMouseExited || eventType == NSEventTypeCursorUpdate;
BOOL isMouseMoveEvent = !isMouseClickEvent && isMouseMoveEventType;
BOOL isTextViewClick = (hitView && hitView == _textView) && !isMouseMoveEvent;

return isTextViewClick ? self : hitView;
}

- (void)mouseDown:(NSEvent *)event
{
if (!_textView.selectable) {
[super mouseDown:event];
return;
}

// Double/triple-clicks should be forwarded to the NSTextView.
BOOL shouldForward = event.clickCount > 1;

if (!shouldForward) {
// Peek at next event to know if a selection should begin.
NSEvent *nextEvent = [self.window nextEventMatchingMask:NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:NO];
shouldForward = nextEvent.type == NSEventTypeLeftMouseDragged;
}

if (shouldForward) {
NSView *contentView = self.window.contentView;
// -[NSView hitTest:] takes coordinates in a view's superview coordinate system.
NSPoint point = [contentView.superview convertPoint:event.locationInWindow fromView:nil];

// Start selection if we're still selectable and hit-testable.
if (_textView.selectable && [contentView hitTest:point] == self) {
[[RCTSurfaceTouchHandler surfaceTouchHandlerForView:self] cancelTouchWithEvent:event];
[self.window makeFirstResponder:_textView];
[_textView mouseDown:event];
}
} else {
// Clear selection for single clicks.
_textView.selectedRange = NSMakeRange(NSNotFound, 0);
}
}

#pragma mark - Selection

- (void)textDidEndEditing:(NSNotification *)notification
{
_textView.selectedRange = NSMakeRange(NSNotFound, 0);
}

#endif // macOS]

#if !TARGET_OS_OSX // [macOS]
- (BOOL)canBecomeFirstResponder
{
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
return paragraphProps.isSelectable;
}
#else
- (BOOL)becomeFirstResponder
{
if (![super becomeFirstResponder]) {
return NO;
}

return YES;
}

- (BOOL)canBecomeFirstResponder
{
return self.focusable;
}
#endif // macOS]

#if !TARGET_OS_OSX // [macOS]
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
Expand All @@ -319,7 +420,11 @@ - (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]

Expand Down Expand Up @@ -357,10 +462,12 @@ - (void)copy:(id)sender
}

@implementation RCTParagraphTextView {
#if !TARGET_OS_OSX // [macOS]
CAShapeLayer *_highlightLayer;
#endif // macOS]
}

- (RCTUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- (RCTUIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event // [macOS]
{
return nil;
}
Expand All @@ -382,6 +489,7 @@ - (void)drawRect:(CGRect)rect

CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());

#if !TARGET_OS_OSX // [macOS]
[nativeTextLayoutManager drawAttributedString:_state->getData().attributedString
paragraphAttributes:_paragraphAttributes
frame:frame
Expand All @@ -393,70 +501,54 @@ - (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;
}
}];
}
#else // [macOS
NSTextStorage *textStorage = [nativeTextLayoutManager getTextStorageForAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes size:frame.size];

@end
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;

[self replaceTextContainer:textContainer];

NSArray<NSLayoutManager *> *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
// Copied from RCTUIKit
CGPathRef UIBezierPathCreateCGPathRef(UIBezierPath *bezierPath)
- (void)setNeedsDisplay
{
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);
[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 immutablePath;

return [super resignFirstResponder];
}
#endif // macOS]

@end
13 changes: 13 additions & 0 deletions packages/react-native/React/Fabric/RCTSurfaceTouchHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

NS_ASSUME_NONNULL_BEGIN

#if TARGET_OS_OSX // [macOS
static NSString *const RCTSurfaceTouchHandlerOutsideViewMouseUpNotification = @"RCTSurfaceTouchHandlerOutsideViewMouseUpNotification";
#endif // macOS]

@interface RCTSurfaceTouchHandler : UIGestureRecognizer

/*
Expand All @@ -23,6 +27,15 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, assign) CGPoint viewOriginOffset;

#if TARGET_OS_OSX // [macOS
+ (instancetype)surfaceTouchHandlerForEvent:(NSEvent *)event;
+ (instancetype)surfaceTouchHandlerForView:(NSView *)view;
+ (void)notifyOutsideViewMouseUp:(NSEvent *)event;

- (void)cancelTouchWithEvent:(NSEvent *)event;
- (void)reset;
#endif // macOS]

@end

NS_ASSUME_NONNULL_END
Loading
Loading