Signal-iOS/SignalUI/Views/OWSBubbleShapeView.swift
Evan Hahn 370ff654e7
Change license to AGPL
Change license to AGPL

This commit:

- Updates the `LICENSE` file

- Start every file with something like:

      // Copyright YEAR_FIRST_PUBLISHED Signal Messenger, LLC
      // SPDX-License-Identifier: AGPL-3.0-only

---

First, I removed existing license headers with this Ruby 3.1.2 script:

    require 'set'

    EXTENSIONS_TO_CHECK = Set['.h', '.hpp', '.cpp', '.m', '.mm', '.pch', '.swift']

    same = 0
    different = 0

    all_files = `git ls-files`.lines.map { |line| line.strip }
    all_files.each do |relative_path|
      if relative_path == 'Pods'
        next
      end

      unless EXTENSIONS_TO_CHECK.include? File.extname(relative_path)
        next
      end

      path = File.expand_path(relative_path)

      contents = File.read(path)
      new_contents = contents.sub(/\/\/\n\/\/  Copyright .*\n\/\/\n\n/, '')

      if contents == new_contents
        same += 1
      else
        different += 1
      end

      File.write(path, new_contents)
    end

    puts "updated #{different} file(s), left #{same} untouched"

I'm sure this script could be improved, but it worked well enough.

Then, I created `Scripts/lint/lint-license-headers` and ran it to auto-
fix a lot of files. This changed the mode of some files, but I think
that's actually desirable. For example,
`SignalServiceKit/src/Util/AppContext.m` previously had a mode of
`0755/-rwxr-xr-x`, and it's now `0644/-rw-r--r--`.

Then I fixed some stragglers and updated the precommit script.

See [a similar change in the Desktop app][0].

[0]: 8bfaf598af
2022-10-13 08:25:37 -05:00

231 lines
7.7 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
// While rendering message bubbles, we often need to render
// into a subregion of the bubble that reflects the intersection
// of some subview (e.g. a media view) and the bubble shape
// (including its rounding).
@objc
public class OWSBubbleShapeView: UIView, OWSBubbleViewPartner {
// This view support multiple kinds of rendering.
public enum Mode: Equatable {
case stroke(strokeColor: UIColor, strokeThickness: CGFloat)
case fill(fillColor: UIColor)
case strokeAndFill(fillColor: UIColor, strokeColor: UIColor, strokeThickness: CGFloat)
// Casts a shadow over a subregion of the bubble shape.
case shadow(fillColor: UIColor?)
// Clipping subviews to subregion of the bubble shape.
case clip
case innerShadow(color: UIColor, radius: CGFloat, opacity: Float)
}
// If we ever make mode a var, we need to clear currentState
// in a didSet clause.
private let mode: Mode
private let shapeLayer = CAShapeLayer()
private let maskLayer = CAShapeLayer()
private weak var bubbleViewHost: OWSBubbleViewHost? {
didSet {
updateLayers()
}
}
private var isConfigured = false
public required init(mode: Mode) {
self.mode = mode
super.init(frame: .zero)
self.isOpaque = false
self.backgroundColor = .clear
self.layoutMargins = .zero
owsAssertDebug(self.layer.delegate === self)
shapeLayer.disableAnimationsWithDelegate()
maskLayer.disableAnimationsWithDelegate()
layer.addSublayer(shapeLayer)
isConfigured = true
updateLayers()
}
@available(*, unavailable, message: "use other constructor instead.")
@objc
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func updateConstraints() {
super.updateConstraints()
deactivateAllConstraints()
}
public override var bounds: CGRect {
didSet {
if oldValue.size != bounds.size {
viewSizeDidChange()
}
}
}
public override var frame: CGRect {
didSet {
if oldValue.size != frame.size {
viewSizeDidChange()
}
}
}
private func viewSizeDidChange() {
updateLayers()
}
// MARK: - OWSBubbleViewPartner
public func setBubbleViewHost(_ bubbleViewHost: OWSBubbleViewHost?) {
self.bubbleViewHost = bubbleViewHost
}
public func updateLayers() {
guard isConfigured,
let bubbleViewHost = bubbleViewHost else {
return
}
// Add the bubble view's path to the local path.
let bubbleBezierPath: UIBezierPath = bubbleViewHost.maskPath
// We need to convert between coordinate systems using layers, not views.
let bubbleOffset: CGPoint = self.convert(CGPoint.zero, from: bubbleViewHost.bubbleReferenceView)
let newState = State(bounds: bounds,
bubbleOffset: bubbleOffset,
bubbleBezierPath: bubbleBezierPath)
guard newState != currentState else {
return
}
currentState = newState
Self.updateLayers(mode: mode,
state: newState,
bubbleShapeView: self)
}
private struct State: Equatable {
let bounds: CGRect
let bubbleOffset: CGPoint
let bubbleBezierPath: UIBezierPath
}
private var currentState: State?
private static func updateLayers(mode: Mode,
state: State,
bubbleShapeView: OWSBubbleShapeView) {
let shapeLayer = bubbleShapeView.shapeLayer
let maskLayer = bubbleShapeView.maskLayer
let bounds = state.bounds
let bubbleOffset = state.bubbleOffset
let bubbleBezierPath = state.bubbleBezierPath
let bezierPath = UIBezierPath()
let transform: CGAffineTransform = CGAffineTransform.translate(bubbleOffset)
bubbleBezierPath.apply(transform)
bezierPath.append(bubbleBezierPath)
func configureForDrawing(fillColor: UIColor? = nil,
strokeColor: UIColor? = nil,
strokeThickness: CGFloat = 0) {
bezierPath.append(UIBezierPath(rect: bounds))
bubbleShapeView.clipsToBounds = true
if let strokeColor = strokeColor {
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.lineWidth = strokeThickness
shapeLayer.zPosition = 100
} else {
shapeLayer.strokeColor = nil
shapeLayer.lineWidth = 0
}
if let fillColor = fillColor {
shapeLayer.fillColor = fillColor.cgColor
} else {
shapeLayer.fillColor = nil
}
shapeLayer.path = bezierPath.cgPath
}
switch mode {
case .stroke(let strokeColor, let strokeThickness):
configureForDrawing(strokeColor: strokeColor, strokeThickness: strokeThickness)
case .fill(let fillColor):
configureForDrawing(fillColor: fillColor)
case .strokeAndFill(let fillColor, let strokeColor, let strokeThickness):
configureForDrawing(fillColor: fillColor, strokeColor: strokeColor, strokeThickness: strokeThickness)
case .shadow(let fillColor):
bubbleShapeView.clipsToBounds = false
if let fillColor = fillColor {
shapeLayer.fillColor = fillColor.cgColor
} else {
shapeLayer.fillColor = nil
}
shapeLayer.path = bezierPath.cgPath
shapeLayer.frame = bounds
shapeLayer.masksToBounds = true
shapeLayer.shadowPath = bezierPath.cgPath
case .clip:
maskLayer.path = bezierPath.cgPath
bubbleShapeView.layer.mask = maskLayer
case .innerShadow(let color, let radius, let opacity):
// Inner shadow.
// This should usually not be visible; it is used to distinguish
// profile pics from the background if they are similar.
shapeLayer.frame = bounds
shapeLayer.masksToBounds = true
let shadowBounds = bounds
let shadowPath = bezierPath.copy() as! UIBezierPath
// This can be any value large enough to cast a sufficiently large shadow.
let shadowInset = radius * -4
let outerRect = shadowBounds.inset(by: UIEdgeInsets(hMargin: shadowInset,
vMargin: shadowInset))
let outerPath = UIBezierPath(rect: outerRect)
// -[CALayer shadowPath] uses the non-zero winding rule
// Reverse the path to make the directions line up correctly.
shadowPath.append(outerPath.reversing())
// This can be any color since the fill should be clipped.
shapeLayer.fillColor = UIColor.black.cgColor
shapeLayer.path = shadowPath.cgPath
shapeLayer.shadowColor = color.cgColor
shapeLayer.shadowRadius = radius
shapeLayer.shadowOpacity = opacity
shapeLayer.shadowOffset = .zero
shapeLayer.shadowPath = shadowPath.cgPath
}
}
// MARK: - CALayerDelegate
@objc
public override func action(for layer: CALayer, forKey event: String) -> CAAction? {
// Disable all implicit CALayer animations.
NSNull()
}
}