blob: f613e1c0cf8fa0e5c744919eac0b9eb7113edcbd [file] [log] [blame]
/*
* Copyright (C) 2014-2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "RemoteLayerTreeViews.h"
#if PLATFORM(IOS_FAMILY)
#import "Logging.h"
#import "RemoteLayerTreeHost.h"
#import "RemoteLayerTreeNode.h"
#import "UIKitSPI.h"
#import "WKDrawingView.h"
#import <WebCore/Region.h>
#import <pal/spi/cocoa/QuartzCoreSPI.h>
#import <wtf/SoftLinking.h>
namespace WebKit {
static void collectDescendantViewsAtPoint(Vector<UIView *, 16>& viewsAtPoint, UIView *parent, CGPoint point, UIEvent *event)
{
if (parent.clipsToBounds && ![parent pointInside:point withEvent:event])
return;
for (UIView *view in [parent subviews]) {
CGPoint subviewPoint = [view convertPoint:point fromView:parent];
auto handlesEvent = [&] {
// FIXME: isUserInteractionEnabled is mostly redundant with event regions for web content layers.
// It is currently only needed for scroll views.
if (!view.isUserInteractionEnabled)
return false;
if (CGRectIsEmpty([view frame]))
return false;
if (![view pointInside:subviewPoint withEvent:event])
return false;
if (![view isKindOfClass:[WKCompositingView class]])
return true;
auto* node = RemoteLayerTreeNode::forCALayer(view.layer);
return node->eventRegion().contains(WebCore::IntPoint(subviewPoint));
}();
if (handlesEvent)
viewsAtPoint.append(view);
if (![view subviews])
return;
collectDescendantViewsAtPoint(viewsAtPoint, view, subviewPoint, event);
};
}
static bool isScrolledBy(WKChildScrollView* scrollView, UIView *hitView)
{
auto scrollLayerID = RemoteLayerTreeNode::layerID(scrollView.layer);
for (UIView *view = hitView; view; view = [view superview]) {
if (view == scrollView)
return true;
auto* node = RemoteLayerTreeNode::forCALayer(view.layer);
if (node && scrollLayerID) {
if (node->actingScrollContainerID() == scrollLayerID)
return true;
if (node->stationaryScrollContainerIDs().contains(scrollLayerID))
return false;
}
}
return false;
}
#if ENABLE(POINTER_EVENTS)
OptionSet<WebCore::TouchAction> touchActionsForPoint(UIView *rootView, const WebCore::IntPoint& point)
{
Vector<UIView *, 16> viewsAtPoint;
collectDescendantViewsAtPoint(viewsAtPoint, rootView, point, nil);
if (viewsAtPoint.isEmpty())
return { WebCore::TouchAction::Auto };
UIView *hitView = nil;
for (auto *view : WTF::makeReversedRange(viewsAtPoint)) {
// We only hit WKChildScrollView directly if its content layer doesn't have an event region.
// We don't generate the region if there is nothing interesting in it, meaning the touch-action is auto.
if ([view isKindOfClass:[WKChildScrollView class]])
return WebCore::TouchAction::Auto;
if ([view isKindOfClass:[WKCompositingView class]]) {
hitView = view;
break;
}
}
if (!hitView)
return { WebCore::TouchAction::Auto };
CGPoint hitViewPoint = [hitView convertPoint:point fromView:rootView];
auto* node = RemoteLayerTreeNode::forCALayer(hitView.layer);
if (!node)
return { WebCore::TouchAction::Auto };
return node->eventRegion().touchActionsForPoint(WebCore::IntPoint(hitViewPoint));
}
#endif
UIScrollView *findActingScrollParent(UIScrollView *scrollView, const RemoteLayerTreeHost& host)
{
HashSet<WebCore::GraphicsLayer::PlatformLayerID> scrollersToSkip;
for (UIView *view = [scrollView superview]; view; view = [view superview]) {
if ([view isKindOfClass:[WKChildScrollView class]] && !scrollersToSkip.contains(RemoteLayerTreeNode::layerID(view.layer))) {
// FIXME: Ideally we would return the scroller we want in all cases but the current UIKit SPI only allows returning a non-ancestor.
return nil;
}
if (auto* node = RemoteLayerTreeNode::forCALayer(view.layer)) {
if (auto* actingParent = host.nodeForID(node->actingScrollContainerID())) {
if ([actingParent->uiView() isKindOfClass:[UIScrollView class]])
return (UIScrollView *)actingParent->uiView();
}
scrollersToSkip.add(node->stationaryScrollContainerIDs().begin(), node->stationaryScrollContainerIDs().end());
}
}
return nil;
}
}
@interface UIView (WKHitTesting)
- (UIView *)_web_findDescendantViewAtPoint:(CGPoint)point withEvent:(UIEvent *)event;
@end
@implementation UIView (WKHitTesting)
- (UIView *)_web_findDescendantViewAtPoint:(CGPoint)point withEvent:(UIEvent *)event
{
Vector<UIView *, 16> viewsAtPoint;
WebKit::collectDescendantViewsAtPoint(viewsAtPoint, self, point, event);
LOG_WITH_STREAM(UIHitTesting, stream << (void*)self << "_web_findDescendantViewAtPoint " << WebCore::FloatPoint(point) << " found " << viewsAtPoint.size() << " views");
for (auto *view : WTF::makeReversedRange(viewsAtPoint)) {
if ([view conformsToProtocol:@protocol(WKNativelyInteractible)]) {
LOG_WITH_STREAM(UIHitTesting, stream << " " << (void*)view << " is natively interactible");
CGPoint subviewPoint = [view convertPoint:point fromView:self];
return [view hitTest:subviewPoint withEvent:event];
}
if ([view isKindOfClass:[WKChildScrollView class]]) {
if (WebKit::isScrolledBy((WKChildScrollView *)view, viewsAtPoint.last())) {
LOG_WITH_STREAM(UIHitTesting, stream << " " << (void*)view << " is child scroll view and scrolled by " << (void*)viewsAtPoint.last());
return view;
}
}
LOG_WITH_STREAM(UIHitTesting, stream << " ignoring " << [view class] << " " << (void*)view);
}
LOG_WITH_STREAM(UIHitTesting, stream << (void*)self << "_web_findDescendantViewAtPoint found no interactive views");
return nil;
}
@end
@implementation WKCompositingView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return [self _web_findDescendantViewAtPoint:point withEvent:event];
}
- (NSString *)description
{
return WebKit::RemoteLayerTreeNode::appendLayerDescription(super.description, self.layer);
}
@end
@implementation WKTransformView
+ (Class)layerClass
{
return [CATransformLayer self];
}
@end
@implementation WKSimpleBackdropView
+ (Class)layerClass
{
return [CABackdropLayer self];
}
@end
@implementation WKShapeView
+ (Class)layerClass
{
return [CAShapeLayer self];
}
@end
@implementation WKRemoteView
- (instancetype)initWithFrame:(CGRect)frame contextID:(uint32_t)contextID
{
if ((self = [super initWithFrame:frame])) {
CALayerHost *layer = (CALayerHost *)self.layer;
layer.contextId = contextID;
#if PLATFORM(MACCATALYST)
// When running iOS apps on macOS, kCAContextIgnoresHitTest isn't respected; instead, we avoid
// hit-testing to the remote context by disabling hit-testing on its host layer. See
// <rdar://problem/40591107> for more details.
layer.allowsHitTesting = NO;
#endif
}
return self;
}
+ (Class)layerClass
{
return NSClassFromString(@"CALayerHost");
}
@end
#if USE(UIREMOTEVIEW_CONTEXT_HOSTING)
@implementation WKUIRemoteView
- (instancetype)initWithFrame:(CGRect)frame pid:(pid_t)pid contextID:(uint32_t)contextID
{
self = [super initWithFrame:frame pid:pid contextID:contextID];
if (!self)
return nil;
#if PLATFORM(MACCATALYST)
// When running iOS apps on macOS, kCAContextIgnoresHitTest isn't respected; instead, we avoid
// hit-testing to the remote context by disabling hit-testing on its host layer. See
// <rdar://problem/40591107> for more details.
self.layerHost.allowsHitTesting = NO;
#endif
return self;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return [self _web_findDescendantViewAtPoint:point withEvent:event];
}
- (NSString *)description
{
return WebKit::RemoteLayerTreeNode::appendLayerDescription(super.description, self.layer);
}
@end
#endif
@implementation WKBackdropView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return [self _web_findDescendantViewAtPoint:point withEvent:event];
}
- (NSString *)description
{
return WebKit::RemoteLayerTreeNode::appendLayerDescription(super.description, self.layer);
}
@end
@implementation WKChildScrollView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (!self)
return nil;
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 110000
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
#endif
return self;
}
@end
@implementation WKEmbeddedView
- (instancetype)initWithEmbeddedViewID:(WebCore::GraphicsLayer::EmbeddedViewID)embeddedViewID
{
self = [super init];
if (!self)
return nil;
_embeddedViewID = embeddedViewID;
return self;
}
@end
#endif // PLATFORM(IOS_FAMILY)