Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/ImageEditor/ImageEditorViewController+Draw.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit

// MARK: - Draw Tool

extension ImageEditorViewController {

    private func initializeDrawToolUIIfNecessary() {
        guard !drawToolUIInitialized else { return }

        view.addSubview(drawToolbar)
        drawToolbar.autoPinWidthToSuperview()
        drawToolbar.autoPinEdge(.bottom, to: .top, of: bottomBar)

        view.addGestureRecognizer(drawToolGestureRecognizer)

        drawToolUIInitialized = true
    }

    func updateDrawToolControlsVisibility() {
        drawToolbar.alpha = topBar.alpha
        strokeWidthSliderContainer.alpha = topBar.alpha
    }

    func updateDrawToolUIVisibility() {
        let visible = mode == .draw

        if visible {
            initializeDrawToolUIIfNecessary()
        } else {
            guard drawToolUIInitialized else { return }
        }

        drawToolbar.isHidden = !visible
        drawToolGestureRecognizer.isEnabled = visible

        if visible {
            currentStrokeType = drawToolbar.strokeTypeButton.isSelected ? .highlighter : .pen
        }
    }

    static var highligherStrokeOpacity: CGFloat = 0.5

    @objc
    func handleDrawToolGesture(_ gestureRecognizer: ImageEditorPanGestureRecognizer) {
        AssertIsOnMainThread()

        owsAssertDebug(mode == .draw, "Incorrect mode [\(mode)]")

        let removeCurrentStroke = {
            if let stroke = self.currentStroke {
                self.model.remove(item: stroke)
            }
            self.currentStroke = nil
            self.currentStrokeSamples.removeAll()
        }
        let tryToAppendStrokeSample = { (locationInView: CGPoint) in
            let view = self.imageEditorView.gestureReferenceView
            let viewBounds = view.bounds
            let newSample = ImageEditorCanvasView.locationImageUnit(
                forLocationInView: locationInView,
                viewBounds: viewBounds,
                model: self.model,
                transform: self.model.currentTransform(),
            )

            if
                let prevSample = self.currentStrokeSamples.last,
                prevSample == newSample
            {
                // Ignore duplicate samples.
                return
            }
            self.currentStrokeSamples.append(newSample)
        }

        var strokeColor = drawToolbar.colorPickerView.selectedValue.color
        if currentStrokeType == .highlighter {
            strokeColor = strokeColor.withAlphaComponent(Self.highligherStrokeOpacity)
        }
        let unitStrokeWidth = currentStrokeUnitWidth()

        switch gestureRecognizer.state {
        case .began:
            setStrokeWidthSlider(revealed: false)

            removeCurrentStroke()

            // Apply the location history of the gesture so that the stroke reflects
            // the touch's movement before the gesture recognized.
            for location in gestureRecognizer.locationHistory {
                tryToAppendStrokeSample(location)
            }

            let locationInView = gestureRecognizer.location(in: imageEditorView.gestureReferenceView)
            tryToAppendStrokeSample(locationInView)

            let stroke = ImageEditorStrokeItem(
                color: strokeColor,
                strokeType: currentStrokeType,
                unitSamples: currentStrokeSamples,
                unitStrokeWidth: unitStrokeWidth,
            )
            model.append(item: stroke)
            currentStroke = stroke

        case .changed, .ended:
            let locationInView = gestureRecognizer.location(in: imageEditorView.gestureReferenceView)
            tryToAppendStrokeSample(locationInView)

            guard let lastStroke = self.currentStroke else {
                owsFailDebug("Missing last stroke.")
                removeCurrentStroke()
                return
            }

            // Model items are immutable; we _replace_ the
            // stroke item rather than modify it.
            let stroke = ImageEditorStrokeItem(
                itemId: lastStroke.itemId,
                color: strokeColor,
                strokeType: currentStrokeType,
                unitSamples: currentStrokeSamples,
                unitStrokeWidth: unitStrokeWidth,
            )
            model.replace(item: stroke, suppressUndo: true)

            if gestureRecognizer.state == .ended {
                currentStroke = nil
                currentStrokeSamples.removeAll()
            } else {
                currentStroke = stroke
            }

        default:
            removeCurrentStroke()
        }
    }

    class DrawToolbar: UIView {

        let colorPickerView: ColorPickerBarView

        let strokeTypeButton = RoundMediaButton(
            image: UIImage(imageLiteralResourceName: "brush-pen"),
            backgroundStyle: .blur,
        )

        init(currentColor: ColorPickerBarColor) {
            self.colorPickerView = ColorPickerBarView(currentColor: currentColor)
            super.init(frame: .zero)

            layoutMargins.top = 0
            layoutMargins.bottom = 2

            strokeTypeButton.setImage(UIImage(imageLiteralResourceName: "brush-highlighter"), for: .selected)

            // A container with width capped at a predefined size,
            // centered in superview and constrained to layout margins.
            let stackViewLayoutGuide = UILayoutGuide()
            addLayoutGuide(stackViewLayoutGuide)
            addConstraints([
                stackViewLayoutGuide.centerXAnchor.constraint(equalTo: centerXAnchor),
                stackViewLayoutGuide.leadingAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leadingAnchor),
                stackViewLayoutGuide.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
                stackViewLayoutGuide.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
            ])
            addConstraint({
                let constraint = stackViewLayoutGuide.widthAnchor.constraint(equalToConstant: ImageEditorViewController.preferredToolbarContentWidth)
                constraint.priority = .defaultHigh
                return constraint
            }())

            // I had to use a custom layout guide because stack view isn't centered
            // but instead has slight offset towards the trailing edge.
            let stackView = UIStackView(arrangedSubviews: [colorPickerView, strokeTypeButton])
            stackView.translatesAutoresizingMaskIntoConstraints = false
            stackView.alignment = .center
            stackView.spacing = 8
            addSubview(stackView)
            addConstraints([
                stackView.leadingAnchor.constraint(equalTo: stackViewLayoutGuide.leadingAnchor),
                stackView.trailingAnchor.constraint(
                    equalTo: stackViewLayoutGuide.trailingAnchor,
                    constant: strokeTypeButton.layoutMargins.trailing,
                ),
                stackView.topAnchor.constraint(equalTo: stackViewLayoutGuide.topAnchor),
                stackView.bottomAnchor.constraint(equalTo: stackViewLayoutGuide.bottomAnchor),
            ])
        }

        @available(iOS, unavailable, message: "Use init(currentColor:)")
        required init(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
}