12
12
#endif // [macOS]
13
13
14
14
#import < React/RCTAssert.h> // [macOS]
15
+ #import < React/RCTUIManager.h> // [macOS]
15
16
#import < React/RCTUtils.h>
16
17
#import < React/UIView+React.h>
17
18
#import < React/RCTFocusChangeEvent.h> // [macOS]
@@ -62,6 +63,8 @@ @implementation RCTTextView {
62
63
63
64
id <RCTEventDispatcherProtocol> _eventDispatcher; // [macOS]
64
65
NSArray <RCTUIView *> *_Nullable _descendantViews; // [macOS]
66
+ NSArray <RCTVirtualTextView *> *_Nullable _virtualSubviews; // [macOS]
67
+ RCTUIView *_Nullable _currentHoveredSubview; // [macOS]
65
68
NSTextStorage *_Nullable _textStorage;
66
69
CGRect _contentFrame;
67
70
}
@@ -99,6 +102,7 @@ - (instancetype)initWithFrame:(CGRect)frame
99
102
_textView.layoutManager .usesFontLeading = NO ;
100
103
_textStorage = _textView.textStorage ;
101
104
[self addSubview: _textView];
105
+ _currentHoveredSubview = nil ;
102
106
#endif // macOS]
103
107
RCTUIViewSetContentModeRedraw (self); // [macOS]
104
108
}
@@ -162,6 +166,20 @@ - (void)setTextStorage:(NSTextStorage *)textStorage
162
166
contentFrame : (CGRect)contentFrame
163
167
descendantViews : (NSArray <RCTPlatformView *> *)descendantViews // [macOS]
164
168
{
169
+ // [macOS - to keep track of virtualSubviews as well
170
+ [self setTextStorage: textStorage
171
+ contentFrame: contentFrame
172
+ descendantViews: descendantViews
173
+ virtualSubviews: nil ];
174
+ }
175
+
176
+ - (void )setTextStorage : (NSTextStorage *)textStorage
177
+ contentFrame : (CGRect)contentFrame
178
+ descendantViews : (NSArray <RCTPlatformView *> *)descendantViews
179
+ virtualSubviews : (NSArray <RCTVirtualTextView *> *)virtualSubviews
180
+ {
181
+ // macOS]
182
+
165
183
// This lets the textView own its text storage on macOS
166
184
// We update and replace the text container `_textView.textStorage.attributedString` when text/layout changes
167
185
#if !TARGET_OS_OSX // [macOS]
@@ -204,6 +222,8 @@ - (void)setTextStorage:(NSTextStorage *)textStorage
204
222
[self addSubview: view];
205
223
}
206
224
225
+ _virtualSubviews = virtualSubviews; // [macOS]
226
+
207
227
[self setNeedsDisplay ];
208
228
}
209
229
@@ -398,6 +418,21 @@ - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
398
418
}
399
419
#else // [macOS
400
420
421
+ - (BOOL )hasMouseHoverEvent
422
+ {
423
+ if ([super hasMouseHoverEvent ]) {
424
+ return YES ;
425
+ }
426
+
427
+ // We only care about virtual subviews here.
428
+ // Embedded views (e.g., <Text> <View /> </Text>) handle mouse hover events themselves.
429
+ NSUInteger indexOfChildWithMouseHoverEvent = [_virtualSubviews indexOfObjectPassingTest: ^BOOL (RCTVirtualTextView *_Nonnull childView, NSUInteger idx, BOOL *_Nonnull stop) {
430
+ *stop = [childView hasMouseHoverEvent ];
431
+ return *stop;
432
+ }];
433
+ return indexOfChildWithMouseHoverEvent != NSNotFound ;
434
+ }
435
+
401
436
- (NSView *)hitTest : (NSPoint )point
402
437
{
403
438
// 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).
@@ -412,6 +447,110 @@ - (NSView *)hitTest:(NSPoint)point
412
447
return isTextViewClick ? self : hitView;
413
448
}
414
449
450
+ - (NSNumber *)reactTagAtMouseLocationFromEvent : (NSEvent *)event
451
+ {
452
+ NSPoint locationInSelf = [self convertPoint: event.locationInWindow fromView: nil ];
453
+ NSPoint locationInInnerTextView = [self convertPoint: locationInSelf toView: _textView]; // This is needed if the parent <Text> view has padding
454
+ return [self reactTagAtPoint: locationInInnerTextView];
455
+ }
456
+
457
+ - (void )mouseEntered : (NSEvent *)event
458
+ {
459
+ // superclass invokes self.onMouseEnter, so do this first
460
+ [super mouseEntered: event];
461
+
462
+ [self updateHoveredSubviewWithEvent: event];
463
+ }
464
+
465
+ - (void )mouseExited : (NSEvent *)event
466
+ {
467
+ [self updateHoveredSubviewWithEvent: event];
468
+
469
+ // superclass invokes self.onMouseLeave, so do this last
470
+ [super mouseExited: event];
471
+ }
472
+
473
+ - (void )mouseMoved : (NSEvent *)event
474
+ {
475
+ [super mouseMoved: event];
476
+ [self updateHoveredSubviewWithEvent: event];
477
+ }
478
+
479
+ - (void )updateHoveredSubviewWithEvent : (NSEvent *)event
480
+ {
481
+ RCTUIView *hoveredView = nil ;
482
+
483
+ if ([event type ] != NSEventTypeMouseExited && _virtualSubviews != nil ) {
484
+ NSNumber *reactTagOfHoveredView = [self reactTagAtMouseLocationFromEvent: event];
485
+
486
+ if (reactTagOfHoveredView == nil ) {
487
+ // This happens if we hover over an embedded view, which will handle its own mouse events
488
+ return ;
489
+ }
490
+
491
+ if ([reactTagOfHoveredView isEqualToNumber: self .reactTag]) {
492
+ // We're hovering over the root Text element
493
+ hoveredView = self;
494
+ } else {
495
+ // Maybe we're hovering over a child Text element?
496
+ NSUInteger index = [_virtualSubviews indexOfObjectPassingTest: ^BOOL (RCTVirtualTextView *_Nonnull view, NSUInteger idx, BOOL *_Nonnull stop) {
497
+ *stop = [[view reactTag ] isEqualToNumber: reactTagOfHoveredView];
498
+ return *stop;
499
+ }];
500
+ if (index != NSNotFound ) {
501
+ hoveredView = _virtualSubviews[index];
502
+ }
503
+ }
504
+ }
505
+
506
+ if (_currentHoveredSubview == hoveredView) {
507
+ return ;
508
+ }
509
+
510
+ // self will always be an ancestor of any views we pass in here, so it serves as a good default option.
511
+ // Also, if we do set from/to nil, we have to call the relevant events on the entire subtree.
512
+ RCTUIManager *uiManager = [[_eventDispatcher bridge ] uiManager ];
513
+ RCTShadowView *oldShadowView = [uiManager shadowViewForReactTag: [(_currentHoveredSubview ?: self ) reactTag ]];
514
+ RCTShadowView *newShadowView = [uiManager shadowViewForReactTag: [(hoveredView ?: self ) reactTag ]];
515
+
516
+ // Find the common ancestor between the two shadow views
517
+ RCTShadowView *commonAncestor = [oldShadowView ancestorSharedWithShadowView: newShadowView];
518
+
519
+ for (RCTShadowView *exitedShadowView = oldShadowView; exitedShadowView != commonAncestor && exitedShadowView != nil ; exitedShadowView = [exitedShadowView reactSuperview ]) {
520
+ RCTPlatformView *exitedView = [uiManager viewForReactTag: [exitedShadowView reactTag ]];
521
+ if (![exitedView isKindOfClass: [RCTUIView class ]]) {
522
+ RCTLogError (@" Unexpected view of type %@ found in hierarchy, must be RCTUIView or subclass" , [exitedView class ]);
523
+ continue ;
524
+ }
525
+
526
+ RCTUIView *exitedReactView = (RCTUIView *)exitedView;
527
+ [self sendMouseEventWithBlock: [exitedReactView onMouseLeave ]
528
+ locationInfo: [self locationInfoFromEvent: event]
529
+ modifierFlags: event.modifierFlags
530
+ additionalData: nil ];
531
+ }
532
+
533
+ // We cache these so we can call them from outermost to innermost
534
+ NSMutableArray <RCTUIView *> *enteredViewHierarchy = [NSMutableArray new ];
535
+ for (RCTShadowView *enteredShadowView = newShadowView; enteredShadowView != commonAncestor && enteredShadowView != nil ; enteredShadowView = [enteredShadowView reactSuperview ]) {
536
+ RCTPlatformView *enteredView = [uiManager viewForReactTag: [enteredShadowView reactTag ]];
537
+ if (![enteredView isKindOfClass: [RCTUIView class ]]) {
538
+ RCTLogError (@" Unexpected view of type %@ found in hierarchy, must be RCTUIView or subclass" , [enteredView class ]);
539
+ continue ;
540
+ }
541
+
542
+ [enteredViewHierarchy addObject: (RCTUIView *)enteredView];
543
+ }
544
+ for (NSInteger i = [enteredViewHierarchy count ] - 1 ; i >= 0 ; i--) {
545
+ [self sendMouseEventWithBlock: [[enteredViewHierarchy objectAtIndex: i] onMouseEnter ]
546
+ locationInfo: [self locationInfoFromEvent: event]
547
+ modifierFlags: event.modifierFlags
548
+ additionalData: nil ];
549
+ }
550
+
551
+ _currentHoveredSubview = hoveredView;
552
+ }
553
+
415
554
- (void )rightMouseDown : (NSEvent *)event
416
555
{
417
556
0 commit comments