Skip to content
Open
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 @@ -88,6 +88,39 @@ static UIScrollViewIndicatorStyle RCTUIScrollViewIndicatorStyleFromProps(const S
userInfo:userInfo];
}

#if TARGET_OS_OSX // [macOS
// AppKit works out where a view's layer goes, flipped or not, when the view's frame is set.
static void RCTRelayoutLayer(RCTPlatformView *view)
{
NSPoint origin = view.frame.origin;
[view setFrameOrigin:NSMakePoint(origin.x + 1, origin.y)];
[view setFrameOrigin:origin];
}

// The document view, which counts from the bottom while the list is inverted.
@interface RCTScrollViewContainerView : RCTUIView
@property (nonatomic, assign) BOOL inverted;
@end

@implementation RCTScrollViewContainerView

- (BOOL)isFlipped
{
return !_inverted;
}

- (void)setInverted:(BOOL)inverted
{
_inverted = inverted;
RCTRelayoutLayer(self);
for (RCTPlatformView *subview in self.subviews) {
RCTRelayoutLayer(subview);
}
}

@end
#endif // macOS]

@interface RCTScrollViewComponentView () <
#if !TARGET_OS_OSX // [macOS]
UIScrollViewDelegate,
Expand All @@ -108,6 +141,9 @@ - (void)_adjustForMaintainVisibleContentPosition;
@end

@implementation RCTScrollViewComponentView {
#if TARGET_OS_OSX // [macOS
BOOL _isInverted;
#endif // macOS]
ScrollViewShadowNode::ConcreteState::Shared _state;
CGSize _contentSize;
NSTimeInterval _lastScrollEventDispatchTime;
Expand Down Expand Up @@ -167,10 +203,11 @@ - (instancetype)initWithFrame:(CGRect)frame
_isAccessibilityAPIUsed = NO;
[self addSubview:_scrollView];

_containerView = [[RCTUIView alloc] initWithFrame:CGRectZero]; // [macOS]
#if !TARGET_OS_OSX // [macOS]
_containerView = [[RCTUIView alloc] initWithFrame:CGRectZero];
[_scrollView addSubview:_containerView];
#else // [macOS
_containerView = [[RCTScrollViewContainerView alloc] initWithFrame:CGRectZero];
// Force overlay scrollbar style to avoid layout issues with legacy scrollbars.
_scrollView.scrollerStyle = NSScrollerStyleOverlay;
// Don't set autoresizingMask — AppKit corrupts the documentView frame during tile/resize.
Expand Down Expand Up @@ -369,6 +406,40 @@ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
}
}

#if TARGET_OS_OSX // [macOS
// An inverted list gives its ScrollView scaleY(-1), as on iOS. Here the document view and the
// content count from the bottom instead (see RCTViewComponentView's invertsSubviewsVertically),
// so AppKit's scrolling, hit-testing and pointer tracking match what is drawn, and a content
// offset still counts from the list's first item.
- (void)setTransform3D:(CATransform3D)transform3D
{
BOOL isInverted = CATransform3DEqualToTransform(transform3D, CATransform3DMakeScale(1, -1, 1));
[super setTransform3D:isInverted ? CATransform3DIdentity : transform3D];
[self applyInversion:isInverted];
}

- (void)applyInversion:(BOOL)isInverted
{
if (_isInverted == isInverted) {
return;
}
_isInverted = isInverted;

CGPoint contentOffset = _scrollView.contentOffset;
((RCTScrollViewContainerView *)_containerView).inverted = isInverted;
if ([_contentView isKindOfClass:[RCTViewComponentView class]]) {
((RCTViewComponentView *)_contentView).invertsSubviewsVertically = isInverted;
}
// The clip view takes in its document's flippedness when the document is set. Scroll events
// sent while it is swapped carry offsets from part-way through, so send the settled one after.
_scrollView.documentView = nil;
_scrollView.documentView = _containerView;
_scrollView.contentOffset = contentOffset;
[self _forceDispatchNextScrollEvent];
[self scrollViewDidScroll:_scrollView];
}
#endif // macOS]

- (bool)isInverted
{
// Look into the entry at position 2,2 to check if scaleY is applied
Expand Down Expand Up @@ -629,11 +700,21 @@ - (void)mountChildComponentView:(RCTUIView<RCTComponentViewProtocol> *)childComp
[_containerView insertSubview:childComponentView atIndex:index];
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)]) {
_contentView = childComponentView;
#if TARGET_OS_OSX // [macOS
if ([childComponentView isKindOfClass:[RCTViewComponentView class]]) {
((RCTViewComponentView *)childComponentView).invertsSubviewsVertically = _isInverted;
}
#endif // macOS]
}
}

- (void)unmountChildComponentView:(RCTUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index // [macOS]
{
#if TARGET_OS_OSX // [macOS
if ([childComponentView isKindOfClass:[RCTViewComponentView class]]) {
((RCTViewComponentView *)childComponentView).invertsSubviewsVertically = NO;
}
#endif // macOS]
[childComponentView removeFromSuperview];
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)] &&
_contentView == childComponentView) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ NS_ASSUME_NONNULL_BEGIN
#if TARGET_OS_OSX // [macOS
- (BOOL)handleKeyboardEvent:(NSEvent *)event;
- (facebook::react::DataTransfer)dataTransferForPasteboard:(NSPasteboard *)pasteboard;

/*
* Lays the subviews out from the bottom, for an inverted ScrollView's content. A subview's own
* scaleY(-1), which an inverted list gives each cell, is then left off.
*/
@property (nonatomic, assign) BOOL invertsSubviewsVertically;
#endif // macOS]

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ @implementation RCTViewComponentView {
BOOL _hasClipViewBoundsObserver;
NSTrackingArea *_trackingArea;
BOOL _allowsVibrancy;
CATransform3D _requestedTransform3D;
#endif // macOS]
NSMutableArray<RCTUIView *> *_reactSubviews; // [macOS]
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
Expand Down Expand Up @@ -89,6 +90,7 @@ - (instancetype)initWithFrame:(CGRect)frame
_removeClippedSubviews = NO;
#if TARGET_OS_OSX // [macOS
_allowsVibrancy = NO;
_requestedTransform3D = CATransform3DIdentity;
self.mouseDownCanMoveWindow = YES;
#endif // macOS]
}
Expand Down Expand Up @@ -2274,6 +2276,73 @@ - (void)viewDidMoveToWindow
[super viewDidMoveToWindow];
}

#pragma mark - Inverted ScrollView content

// AppKit hit-tests, tracks the pointer and scrolls in view coordinates, which leave out layer
// transforms, so an inverted list can't be flipped with scaleY(-1) as on iOS. Its ScrollView lays
// the content out from the bottom instead (RCTScrollViewComponentView), and the cells' flips are
// left off. The flips stay in the props, so layout and measurement see what iOS sees.

static BOOL RCTIsVerticalFlip(CATransform3D transform)
{
return CATransform3DEqualToTransform(transform, CATransform3DMakeScale(1, -1, 1));
}

// AppKit works out where a view's layer goes, flipped or not, when the view's frame is set.
static void RCTRelayoutLayer(RCTPlatformView *view)
{
NSPoint origin = view.frame.origin;
[view setFrameOrigin:NSMakePoint(origin.x + 1, origin.y)];
[view setFrameOrigin:origin];
}

- (BOOL)isFlipped
{
return !_invertsSubviewsVertically;
}

- (void)setInvertsSubviewsVertically:(BOOL)invertsSubviewsVertically
{
if (_invertsSubviewsVertically == invertsSubviewsVertically) {
return;
}
_invertsSubviewsVertically = invertsSubviewsVertically;
RCTRelayoutLayer(self);
for (RCTPlatformView *subview in self.subviews) {
if ([subview isKindOfClass:[RCTViewComponentView class]]) {
[(RCTViewComponentView *)subview applyTransform3DForSuperview];
}
RCTRelayoutLayer(subview);
}
}

- (void)setTransform3D:(CATransform3D)transform3D
{
_requestedTransform3D = transform3D;
[super setTransform3D:[self transform3DForSuperview]];
}

- (CATransform3D)transform3DForSuperview
{
BOOL isInvertedCell = RCTIsVerticalFlip(_requestedTransform3D) &&
[self.superview isKindOfClass:[RCTViewComponentView class]] &&
((RCTViewComponentView *)self.superview).invertsSubviewsVertically;
return isInvertedCell ? CATransform3DIdentity : _requestedTransform3D;
}

- (void)applyTransform3DForSuperview
{
if (RCTIsVerticalFlip(_requestedTransform3D)) {
[super setTransform3D:[self transform3DForSuperview]];
}
}

- (void)viewDidMoveToSuperview
{
[super viewDidMoveToSuperview];
[self applyTransform3DForSuperview];
}

- (void)updateTrackingAreas
{
BOOL hasMouseEventHandler =
Expand Down
6 changes: 4 additions & 2 deletions packages/virtualized-lists/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -962,9 +962,11 @@ class VirtualizedList extends StateSafePureComponent<
const {ListEmptyComponent, ListFooterComponent, ListHeaderComponent} =
this.props;
const {data, horizontal} = this.props;
// macOS natively supports inverted lists, thus not needing an inversion style
// macOS's old architecture inverts a ScrollView natively, so needs no inversion style. On the
// new architecture its ScrollView takes the style as iOS does, and inverts natively from it.
const inversionStyle =
this.props.inverted && Platform.OS !== 'macos' // [macOS]
this.props.inverted &&
(Platform.OS !== 'macos' || global.nativeFabricUIManager != null) // [macOS]
? horizontalOrDefault(this.props.horizontal)
? styles.horizontallyInverted
: styles.verticallyInverted
Expand Down