TM-SGNL-iOS/SignalServiceKit/Util/UIView+OWS.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

382 lines
9.6 KiB
Swift

//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public extension UIEdgeInsets {
init(top: CGFloat, leading: CGFloat, bottom: CGFloat, trailing: CGFloat) {
self.init(top: top,
left: CurrentAppContext().isRTL ? trailing : leading,
bottom: bottom,
right: CurrentAppContext().isRTL ? leading : trailing)
}
init(hMargin: CGFloat, vMargin: CGFloat) {
self.init(top: vMargin, left: hMargin, bottom: vMargin, right: hMargin)
}
init(margin: CGFloat) {
self.init(top: margin, left: margin, bottom: margin, right: margin)
}
func plus(_ inset: CGFloat) -> UIEdgeInsets {
var newInsets = self
newInsets.top += inset
newInsets.bottom += inset
newInsets.left += inset
newInsets.right += inset
return newInsets
}
func minus(_ inset: CGFloat) -> UIEdgeInsets {
plus(-inset)
}
var asSize: CGSize {
CGSize(width: left + right,
height: top + bottom)
}
}
public extension NSDirectionalEdgeInsets {
init(hMargin: CGFloat, vMargin: CGFloat) {
self.init(top: vMargin, leading: hMargin, bottom: vMargin, trailing: hMargin)
}
init(margin: CGFloat) {
self.init(top: margin, leading: margin, bottom: margin, trailing: margin)
}
}
// MARK: -
public extension CGPoint {
func toUnitCoordinates(viewBounds: CGRect, shouldClamp: Bool) -> CGPoint {
CGPoint(x: (x - viewBounds.origin.x).inverseLerp(0, viewBounds.width, shouldClamp: shouldClamp),
y: (y - viewBounds.origin.y).inverseLerp(0, viewBounds.height, shouldClamp: shouldClamp))
}
func toUnitCoordinates(viewSize: CGSize, shouldClamp: Bool) -> CGPoint {
toUnitCoordinates(viewBounds: CGRect(origin: .zero, size: viewSize), shouldClamp: shouldClamp)
}
func fromUnitCoordinates(viewBounds: CGRect) -> CGPoint {
CGPoint(x: viewBounds.origin.x + x.lerp(0, viewBounds.size.width),
y: viewBounds.origin.y + y.lerp(0, viewBounds.size.height))
}
func fromUnitCoordinates(viewSize: CGSize) -> CGPoint {
fromUnitCoordinates(viewBounds: CGRect(origin: .zero, size: viewSize))
}
func inverse() -> CGPoint {
CGPoint(x: -x, y: -y)
}
func plus(_ value: CGPoint) -> CGPoint {
CGPoint.add(self, value)
}
func plusX(_ value: CGFloat) -> CGPoint {
CGPoint.add(self, CGPoint(x: value, y: 0))
}
func plusY(_ value: CGFloat) -> CGPoint {
CGPoint.add(self, CGPoint(x: 0, y: value))
}
func minus(_ value: CGPoint) -> CGPoint {
CGPoint.subtract(self, value)
}
func times(_ value: CGFloat) -> CGPoint {
CGPoint(x: x * value, y: y * value)
}
func min(_ value: CGPoint) -> CGPoint {
// We use "Swift" to disambiguate the global function min() from this method.
CGPoint(x: Swift.min(x, value.x),
y: Swift.min(y, value.y))
}
func max(_ value: CGPoint) -> CGPoint {
// We use "Swift" to disambiguate the global function max() from this method.
CGPoint(x: Swift.max(x, value.x),
y: Swift.max(y, value.y))
}
var length: CGFloat {
sqrt(x * x + y * y)
}
@inlinable
func distance(_ other: CGPoint) -> CGFloat {
sqrt(pow(x - other.x, 2) + pow(y - other.y, 2))
}
@inlinable
func within(_ delta: CGFloat, of other: CGPoint) -> Bool {
distance(other) <= delta
}
static let unit: CGPoint = CGPoint(x: 1.0, y: 1.0)
static let unitMidpoint: CGPoint = CGPoint(x: 0.5, y: 0.5)
func applyingInverse(_ transform: CGAffineTransform) -> CGPoint {
applying(transform.inverted())
}
func fuzzyEquals(_ other: CGPoint, tolerance: CGFloat = 0.001) -> Bool {
(x.fuzzyEquals(other.x, tolerance: tolerance) &&
y.fuzzyEquals(other.y, tolerance: tolerance))
}
static func tan(angle: CGFloat) -> CGPoint {
CGPoint(x: sin(angle),
y: cos(angle))
}
func clamp(_ rect: CGRect) -> CGPoint {
CGPoint(x: x.clamp(rect.minX, rect.maxX),
y: y.clamp(rect.minY, rect.maxY))
}
static func + (left: CGPoint, right: CGPoint) -> CGPoint {
left.plus(right)
}
static func += (left: inout CGPoint, right: CGPoint) {
left.x += right.x
left.y += right.y
}
static func - (left: CGPoint, right: CGPoint) -> CGPoint {
CGPoint(x: left.x - right.x, y: left.y - right.y)
}
static func * (left: CGPoint, right: CGFloat) -> CGPoint {
CGPoint(x: left.x * right, y: left.y * right)
}
static func *= (left: inout CGPoint, right: CGFloat) {
left.x *= right
left.y *= right
}
}
// MARK: -
public extension CGSize {
var aspectRatio: CGFloat {
guard self.height > 0 else {
return 0
}
return self.width / self.height
}
var asPoint: CGPoint {
CGPoint(x: width, y: height)
}
var ceil: CGSize {
CGSize.ceil(self)
}
var floor: CGSize {
CGSize.floor(self)
}
var round: CGSize {
CGSize.round(self)
}
var abs: CGSize {
CGSize(width: Swift.abs(width), height: Swift.abs(height))
}
var largerAxis: CGFloat {
Swift.max(width, height)
}
var smallerAxis: CGFloat {
min(width, height)
}
var isNonEmpty: Bool {
width > 0 && height > 0
}
init(square: CGFloat) {
self.init(width: square, height: square)
}
func plus(_ value: CGSize) -> CGSize {
CGSize.add(self, value)
}
func max(_ other: CGSize) -> CGSize {
return CGSize(width: Swift.max(self.width, other.width),
height: Swift.max(self.height, other.height))
}
static func square(_ size: CGFloat) -> CGSize {
CGSize(width: size, height: size)
}
static func + (left: CGSize, right: CGSize) -> CGSize {
left.plus(right)
}
static func - (left: CGSize, right: CGSize) -> CGSize {
CGSize(width: left.width - right.width,
height: left.height - right.height)
}
static func * (left: CGSize, right: CGFloat) -> CGSize {
CGSize(width: left.width * right,
height: left.height * right)
}
}
// MARK: -
public extension CGRect {
var x: CGFloat {
get {
origin.x
}
set {
origin.x = newValue
}
}
var y: CGFloat {
get {
origin.y
}
set {
origin.y = newValue
}
}
var center: CGPoint {
CGPoint(x: midX, y: midY)
}
var topLeft: CGPoint {
origin
}
var topRight: CGPoint {
CGPoint(x: maxX, y: minY)
}
var bottomLeft: CGPoint {
CGPoint(x: minX, y: maxY)
}
var bottomRight: CGPoint {
CGPoint(x: maxX, y: maxY)
}
func pinnedToVerticalEdge(of boundingRect: CGRect) -> CGRect {
var newRect = self
// If we're positioned outside of the vertical bounds,
// we need to move to the nearest bound
let positionedOutOfVerticalBounds = newRect.minY < boundingRect.minY || newRect.maxY > boundingRect.maxY
// If we're position anywhere but exactly at the vertical
// edges (left and right of bounding rect), we need to
// move to the nearest edge
let positionedAwayFromVerticalEdges = boundingRect.minX != newRect.minX && boundingRect.maxX != newRect.maxX
if positionedOutOfVerticalBounds {
let distanceFromTop = newRect.minY - boundingRect.minY
let distanceFromBottom = boundingRect.maxY - newRect.maxY
if distanceFromTop > distanceFromBottom {
newRect.origin.y = boundingRect.maxY - newRect.height
} else {
newRect.origin.y = boundingRect.minY
}
}
if positionedAwayFromVerticalEdges {
let distanceFromLeading = newRect.minX - boundingRect.minX
let distanceFromTrailing = boundingRect.maxX - newRect.maxX
if distanceFromLeading > distanceFromTrailing {
newRect.origin.x = boundingRect.maxX - newRect.width
} else {
newRect.origin.x = boundingRect.minX
}
}
return newRect
}
}
// MARK: -
public extension UIEdgeInsets {
var totalWidth: CGFloat {
left + right
}
var totalHeight: CGFloat {
top + bottom
}
var totalSize: CGSize {
CGSize(width: totalWidth, height: totalHeight)
}
var leading: CGFloat {
get { CurrentAppContext().isRTL ? right : left }
set {
if CurrentAppContext().isRTL {
right = newValue
} else {
left = newValue
}
}
}
var trailing: CGFloat {
get { CurrentAppContext().isRTL ? left : right }
set {
if CurrentAppContext().isRTL {
left = newValue
} else {
right = newValue
}
}
}
var isNonEmpty: Bool {
left != 0 || right != 0 || top != 0 || bottom != 0
}
}
// MARK: -
public extension CGFloat {
var pointsAsPixels: CGFloat {
self * UIScreen.main.scale
}
// An epsilon is a small, non-zero value.
//
// This value is _NOT_ an appropriate tolerance for fuzzy comparison,
// e.g. fuzzyEquals().
static var epsilon: CGFloat {
// ulpOfOne is the difference between 1.0 and the next largest CGFloat value.
.ulpOfOne
}
}