WKFormColorPicker.mm   [plain text]


/*
 * Copyright (C) 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 "WKFormColorPicker.h"

#if ENABLE(INPUT_TYPE_COLOR) && PLATFORM(IOS_FAMILY)

#import "FocusedElementInformation.h"
#import "UserInterfaceIdiom.h"
#import "WKContentViewInteraction.h"
#import "WKFormPopover.h"
#import "WebPageProxy.h"

#import <WebCore/Color.h>
#import <WebCore/ColorSerialization.h>
#import <wtf/SoftLinking.h>

SOFT_LINK_PRIVATE_FRAMEWORK(PencilKit)
SOFT_LINK_CLASS(PencilKit, PKColorMatrixView)

static const CGFloat additionalKeyboardAffordance = 80;
static const CGFloat colorSelectionIndicatorBorderWidth = 4;
static const CGFloat colorSelectionIndicatorCornerRadius = 9;
static const CGFloat pickerWidthForPopover = 280;
static const CGFloat topColorMatrixPadding = 5;
#if ENABLE(DATALIST_ELEMENT)
static const size_t maxColorSuggestions = 12;
#endif

using namespace WebKit;

#pragma mark - PKColorMatrixView

@interface PKColorMatrixView
+ (NSArray<NSArray<UIColor *> *> *)defaultColorMatrix;
@end

#pragma mark - WKColorButton

@interface WKColorButton : UIButton
@property (nonatomic, strong) UIColor *color;

+ (instancetype)colorButtonWithColor:(UIColor *)color;
@end

@implementation WKColorButton

+ (instancetype)colorButtonWithColor:(UIColor *)color
{
    WKColorButton *colorButton = [WKColorButton buttonWithType:UIButtonTypeCustom];
    colorButton.color = color;
    colorButton.backgroundColor = color;
    return colorButton;
}

- (void)dealloc
{
    [_color release];
    _color = nil;

    [super dealloc];
}

@end

#pragma mark - WKColorMatrixView

@interface WKColorMatrixView : UIView {
    RetainPtr<NSArray<NSArray<UIColor *> *>> _colorMatrix;
    RetainPtr<NSArray<NSArray<WKColorButton *> *>> _colorButtons;
}

@property (nonatomic, weak) id<WKColorMatrixViewDelegate> delegate;

- (instancetype)initWithFrame:(CGRect)frame colorMatrix:(NSArray<NSArray<UIColor *> *> *)matrix;
@end

@implementation WKColorMatrixView

- (instancetype)initWithFrame:(CGRect)frame
{
    return [self initWithFrame:frame colorMatrix:[getPKColorMatrixViewClass() defaultColorMatrix]];
}

- (instancetype)initWithFrame:(CGRect)frame colorMatrix:(NSArray<NSArray<UIColor *> *> *)matrix
{
    if (!(self = [super initWithFrame:frame]))
        return nil;

    _colorMatrix = matrix;

    NSMutableArray *colorButtons = [NSMutableArray array];
    for (NSUInteger row = 0; row < [_colorMatrix count]; row++) {
        NSMutableArray *buttons = [NSMutableArray array];
        for (NSUInteger col = 0; col < [_colorMatrix.get()[0] count]; col++) {
            WKColorButton *button = [WKColorButton colorButtonWithColor:_colorMatrix.get()[row][col]];
            [button addTarget:self action:@selector(colorButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
            [buttons addObject:button];
            [self addSubview:button];
        }
        [colorButtons addObject:buttons];
    }
    _colorButtons = colorButtons;

    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    CGSize matrixSize = self.bounds.size;
    CGFloat numRows = [_colorMatrix count];
    CGFloat numCols = [_colorMatrix.get()[0] count];
    CGFloat buttonHeight = matrixSize.height / numRows;
    CGFloat buttonWidth = matrixSize.width / numCols;

    for (NSUInteger row = 0; row < numRows; row++) {
        for (NSUInteger col = 0; col < numCols; col++) {
            WKColorButton *button = _colorButtons.get()[row][col];
            button.frame = CGRectMake(col * buttonWidth, row * buttonHeight, buttonWidth, buttonHeight);
        }
    }

    [self.delegate colorMatrixViewDidLayoutSubviews:self];
}

- (void)colorButtonTapped:(WKColorButton *)colorButton
{
    [self.delegate colorMatrixView:self didTapColorButton:colorButton];
}

@end

#pragma mark - WKFormColorPicker

@implementation WKColorPicker {
    WKContentView *_view;
    __weak WKColorPopover *_popover;
    RetainPtr<UIView> _colorPicker;

    RetainPtr<UIView> _colorSelectionIndicator;
    RetainPtr<CAShapeLayer> _colorSelectionIndicatorBorder;

    RetainPtr<WKColorMatrixView> _topColorMatrix;
    RetainPtr<WKColorMatrixView> _mainColorMatrix;
    WeakObjCPtr<WKColorButton> _selectedColorButton;

    RetainPtr<UIPanGestureRecognizer> _colorPanGR;
}

+ (NSArray<NSArray<UIColor *> *> *)defaultTopColorMatrix
{
    return @[ @[ UIColor.redColor, UIColor.orangeColor, UIColor.yellowColor, UIColor.greenColor, UIColor.cyanColor, UIColor.blueColor, UIColor.magentaColor, UIColor.purpleColor, UIColor.brownColor, UIColor.whiteColor, UIColor.grayColor, UIColor.blackColor ] ];
}

- (instancetype)initWithView:(WKContentView *)view
{
    return [self initWithView:view inPopover:nil];
}

- (instancetype)initWithView:(WKContentView *)view inPopover:(WKColorPopover *)popover
{
    if (!(self = [super init]))
        return nil;

    _view = view;
    
    _popover = popover;

    CGSize colorPickerSize;
    if (currentUserInterfaceIdiomIsPadOrMac())
        colorPickerSize = CGSizeMake(pickerWidthForPopover, pickerWidthForPopover);
    else {
        auto keyboardSize = [UIKeyboard defaultSizeForInterfaceOrientation:view.interfaceOrientation];
        colorPickerSize = CGSizeMake(keyboardSize.width, keyboardSize.height + additionalKeyboardAffordance);
    }

    _colorPicker = adoptNS([[UIView alloc] initWithFrame:CGRectMake(0, 0, colorPickerSize.width, colorPickerSize.height)]);

    CGFloat totalRows = [[getPKColorMatrixViewClass() defaultColorMatrix] count] + 1;
    CGFloat swatchHeight = (colorPickerSize.height - topColorMatrixPadding) / totalRows;

    _mainColorMatrix = adoptNS([[WKColorMatrixView alloc] initWithFrame:CGRectMake(0, swatchHeight + topColorMatrixPadding, colorPickerSize.width, swatchHeight * (totalRows - 1))]);
    [_mainColorMatrix setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
    [_mainColorMatrix setDelegate:self];
    [_colorPicker addSubview:_mainColorMatrix.get()];

    NSArray<NSArray<UIColor *> *> *topColorMatrix = [[self class] defaultTopColorMatrix];

#if ENABLE(DATALIST_ELEMENT)
    size_t numColorSuggestions = view.focusedElementInformation.suggestedColors.size();
    if (numColorSuggestions) {
        NSMutableArray<UIColor *> *colors = [NSMutableArray array];
        for (size_t i = 0; i < std::min(numColorSuggestions, maxColorSuggestions); i++) {
            WebCore::Color color = view.focusedElementInformation.suggestedColors[i];
            [colors addObject:[UIColor colorWithCGColor:cachedCGColor(color)]];
        }
        topColorMatrix = @[ colors ];
    }
#endif

    _topColorMatrix = adoptNS([[WKColorMatrixView alloc] initWithFrame:CGRectMake(0, 0, colorPickerSize.width, swatchHeight) colorMatrix:topColorMatrix]);
    [_topColorMatrix setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
    [_topColorMatrix setDelegate:self];
    [_colorPicker addSubview:_topColorMatrix.get()];

    _colorSelectionIndicator = adoptNS([[UIView alloc] initWithFrame:CGRectZero]);
    [_colorSelectionIndicator setClipsToBounds:YES];
    [_colorPicker addSubview:_colorSelectionIndicator.get()];

    _colorSelectionIndicatorBorder = adoptNS([[CAShapeLayer alloc] init]);
    [_colorSelectionIndicatorBorder setLineWidth:colorSelectionIndicatorBorderWidth];
    [_colorSelectionIndicatorBorder setStrokeColor:[UIColor whiteColor].CGColor];
    [_colorSelectionIndicatorBorder setFillColor:[UIColor clearColor].CGColor];
    [[_colorSelectionIndicator layer] addSublayer:_colorSelectionIndicatorBorder.get()];

    _colorPanGR = adoptNS([[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPanColors:)]);
    [_colorPicker addGestureRecognizer:_colorPanGR.get()];

    return self;
}

- (void)drawSelectionIndicatorForColorButton:(WKColorButton *)colorButton
{
    _selectedColorButton = colorButton;

    CGRect frame = [_colorPicker convertRect:colorButton.bounds fromView:colorButton];
    [_colorSelectionIndicator setFrame:frame];

    UIRectCorner roundCorner = 0;
    if (currentUserInterfaceIdiomIsPadOrMac()) {
        CGRect colorPickerBounds = [_colorPicker bounds];

        bool minXEqual = std::abs(CGRectGetMinX(frame) - CGRectGetMinX(colorPickerBounds)) < FLT_EPSILON;
        bool minYEqual = std::abs(CGRectGetMinY(frame) - CGRectGetMinY(colorPickerBounds)) < FLT_EPSILON;
        bool maxXEqual = std::abs(CGRectGetMaxX(frame) - CGRectGetMaxX(colorPickerBounds)) < FLT_EPSILON;
        bool maxYEqual = std::abs(CGRectGetMaxY(frame) - CGRectGetMaxY(colorPickerBounds)) < FLT_EPSILON;

        // On iPad, round the corners of the indicator that border the corners of the picker, to match the popover.
        if (minXEqual && minYEqual)
            roundCorner |= UIRectCornerTopLeft;
        if (maxXEqual && minYEqual)
            roundCorner |= UIRectCornerTopRight;
        if (minXEqual && maxYEqual)
            roundCorner |= UIRectCornerBottomLeft;
        if (maxXEqual && maxYEqual)
            roundCorner |= UIRectCornerBottomRight;
    }

    UIBezierPath *cornerMaskPath = [UIBezierPath bezierPathWithRoundedRect:colorButton.bounds byRoundingCorners:roundCorner cornerRadii:CGSizeMake(colorSelectionIndicatorCornerRadius, colorSelectionIndicatorCornerRadius)];
    [_colorSelectionIndicatorBorder setFrame:colorButton.bounds];
    [_colorSelectionIndicatorBorder setPath:cornerMaskPath.CGPath];
}

- (void)setControlValueFromUIColor:(UIColor *)uiColor
{
    WebCore::Color color(uiColor.CGColor);
    [_view page]->setFocusedElementValue(WebCore::serializationForHTML(color));
}

#pragma mark WKFormControl

- (UIView *)controlView
{
    return _colorPicker.get();
}

- (void)controlBeginEditing
{
}

- (void)controlEndEditing
{
}

#pragma mark WKColorMatrixViewDelegate

- (void)colorMatrixViewDidLayoutSubviews:(WKColorMatrixView *)matrixView
{
    if ([_selectedColorButton superview] == matrixView)
        [self drawSelectionIndicatorForColorButton:_selectedColorButton.get().get()];
}

- (void)colorMatrixView:(WKColorMatrixView *)matrixView didTapColorButton:(WKColorButton *)colorButton
{
    if (_selectedColorButton.get().get() == colorButton)
        return;

    [self drawSelectionIndicatorForColorButton:colorButton];
    [self setControlValueFromUIColor:colorButton.color];
#if PLATFORM(MACCATALYST)
    [_popover dismissPopoverAnimated:NO];
    [_view accessoryDone];
#endif
}

#pragma mark UIPanGestureRecognizer

- (void)didPanColors:(UIGestureRecognizer *)gestureRecognizer
{
    CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view];
    UIView *view = [gestureRecognizer.view hitTest:location withEvent:nil];
    if ([view isKindOfClass:[WKColorButton class]]) {
        WKColorButton *colorButton = (WKColorButton *)view;
        if (_selectedColorButton.get().get() == colorButton)
            return;

        [self drawSelectionIndicatorForColorButton:colorButton];
        [self setControlValueFromUIColor:colorButton.color];
    }
}

@end

#endif // ENABLE(INPUT_TYPE_COLOR) && PLATFORM(IOS_FAMILY)