TM-SGNL-iOS/SignalUI/ImageEditor/ImageEditorViewController+Draw.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

188 lines
7.4 KiB
Swift

//
// 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")
}
}
}