Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create non-@MainActor-bound methods for rounding to pixel at scale #140

Merged
merged 4 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 140 additions & 36 deletions Paralayout/AspectRatio.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {

/// An inverted representation of the AspectRatio.
public var inverted: AspectRatio {
return AspectRatio(width: ratioHeight, height: ratioWidth)
AspectRatio(width: ratioHeight, height: ratioWidth)
}

// MARK: - Life Cycle
Expand Down Expand Up @@ -74,29 +74,29 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
// MARK: - Comparable

public static func == (lhs: AspectRatio, rhs: AspectRatio) -> Bool {
return (lhs.ratioWidth * rhs.ratioHeight == lhs.ratioHeight * rhs.ratioWidth)
(lhs.ratioWidth * rhs.ratioHeight == lhs.ratioHeight * rhs.ratioWidth)
}

public static func < (lhs: AspectRatio, rhs: AspectRatio) -> Bool {
return (lhs.ratioWidth * rhs.ratioHeight < lhs.ratioHeight * rhs.ratioWidth)
(lhs.ratioWidth * rhs.ratioHeight < lhs.ratioHeight * rhs.ratioWidth)
}

public static func <= (lhs: AspectRatio, rhs: AspectRatio) -> Bool {
return (lhs.ratioWidth * rhs.ratioHeight <= lhs.ratioHeight * rhs.ratioWidth)
(lhs.ratioWidth * rhs.ratioHeight <= lhs.ratioHeight * rhs.ratioWidth)
}

public static func >= (lhs: AspectRatio, rhs: AspectRatio) -> Bool {
return (lhs.ratioWidth * rhs.ratioHeight >= lhs.ratioHeight * rhs.ratioWidth)
(lhs.ratioWidth * rhs.ratioHeight >= lhs.ratioHeight * rhs.ratioWidth)
}

public static func > (lhs: AspectRatio, rhs: AspectRatio) -> Bool {
return (lhs.ratioWidth * rhs.ratioHeight > lhs.ratioHeight * rhs.ratioWidth)
(lhs.ratioWidth * rhs.ratioHeight > lhs.ratioHeight * rhs.ratioWidth)
}

// MARK: - DebugStringConvertible

public var debugDescription: String {
return ("AspectRatio<\(ratioWidth):\(ratioHeight)>")
("AspectRatio<\(ratioWidth):\(ratioHeight)>")
}

// MARK: - Public Methods
Expand All @@ -107,7 +107,15 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter scaleFactor: The view/window/screen to use for pixel rounding.
@MainActor
public func height(forWidth width: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGFloat {
return (ratioHeight * width / ratioWidth).roundedToPixel(in: scaleFactor)
self.height(forWidth: width, in: scaleFactor.pixelsPerPoint)
}

/// Returns the height of the aspect ratio for a given `width` rounded to the nearest pixel.
///
/// - parameter width: The desired width.
/// - parameter scale: The number of pixels per point.
public func height(forWidth width: CGFloat, in scale: CGFloat) -> CGFloat {
(ratioHeight * width / ratioWidth).roundedToPixel(in: scale)
}

/// Returns the width of the aspect ratio for a given `height` rounded to the nearest pixel.
Expand All @@ -116,7 +124,15 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter scaleFactor: The view/window/screen to use for pixel rounding.
@MainActor
public func width(forHeight height: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGFloat {
return (ratioWidth * height / ratioHeight).roundedToPixel(in: scaleFactor)
self.width(forHeight: height, in: scaleFactor.pixelsPerPoint)
}

/// Returns the width of the aspect ratio for a given `height` rounded to the nearest pixel.
///
/// - parameter height: The desired height.
/// - parameter scale: The number of pixels per point.
public func width(forHeight height: CGFloat, in scale: CGFloat) -> CGFloat {
(ratioWidth * height / ratioHeight).roundedToPixel(in: scale)
}

/// Returns a size of the aspect ratio with the specified `width`. The size's `height` will be rounded to the
Expand All @@ -126,9 +142,18 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter scaleFactor: The view/window/screen to use for pixel rounding.
@MainActor
public func size(forWidth width: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(
self.size(forWidth: width, in: scaleFactor.pixelsPerPoint)
}

/// Returns a size of the aspect ratio with the specified `width`. The size's `height` will be rounded to the
/// nearest pixel.
///
/// - parameter width: The desired width.
/// - parameter scale: The number of pixels per point.
public func size(forWidth width: CGFloat, in scale: CGFloat) -> CGSize {
CGSize(
width: width,
height: height(forWidth: width, in: scaleFactor)
height: height(forWidth: width, in: scale)
)
}

Expand All @@ -139,8 +164,17 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter scaleFactor: The view/window/screen to use for pixel rounding.
@MainActor
public func size(forHeight height: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(
width: width(forHeight: height, in: scaleFactor),
self.size(forHeight: height, in: scaleFactor.pixelsPerPoint)
}

/// Returns a size of the aspect ratio with the specified `height`. The size's `width` will be rounded to the
/// nearest pixel.
///
/// - parameter height: The desired height.
/// - parameter scale: The number of pixels per point.
public func size(forHeight height: CGFloat, in scale: CGFloat) -> CGSize {
CGSize(
width: width(forHeight: height, in: scale),
height: height
)
}
Expand All @@ -153,15 +187,29 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - returns: A size with the receiver's aspect ratio, no larger than the bounding size.
@MainActor
public func size(toFit size: CGSize, in scaleFactor: ScaleFactorProviding) -> CGSize {
self.size(toFit: size, in: scaleFactor.pixelsPerPoint)
}

/// An "aspect-fit" function that determines the largest size of the receiver's aspect ratio that fits within a
/// size.
///
/// - parameter size: The bounding size.
/// - parameter scale: The number of pixels per point.
/// - returns: A size with the receiver's aspect ratio, no larger than the bounding size.
public func size(toFit size: CGSize, in scale: CGFloat) -> CGSize {
if size.aspectRatio <= self {
// Match width, narrow the height.
let fitHeight = min(size.height, height(forWidth: size.width, in: scaleFactor))
return CGSize(width: size.width, height: fitHeight)
CGSize(
width: size.width,
height: min(size.height, height(forWidth: size.width, in: scale))
)

} else {
// Match height, narrow the width.
let fitWidth = min(size.width, width(forHeight: size.height, in: scaleFactor))
return CGSize(width: fitWidth, height: size.height)
CGSize(
width: min(size.width, width(forHeight: size.height, in: scale)),
height: size.height
)
}
}

Expand All @@ -181,7 +229,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
in scaleFactor: ScaleFactorProviding,
layoutDirection: UIUserInterfaceLayoutDirection
) -> CGRect {
return CGRect(
CGRect(
size: size(toFit: rect.size, in: scaleFactor),
at: position,
of: rect,
Expand All @@ -196,20 +244,21 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter rect: The bounding rect.
/// - parameter position: The location within the bounding rect for the new rect, determining where margin(s) will
/// be if the aspect ratios do not match perfectly.
/// - parameter context: The view/window/screen that provides the scale factor and effective layout direction in
/// which the rect should be positioned.
/// - parameter scale: The number of pixels per point.
/// - parameter layoutDirection: The effective layout direction of the view in which the `rect` is defined.
/// - returns: A rect with the receiver's aspect ratio, strictly within the bounding rect.
@MainActor
public func rect(
toFit rect: CGRect,
at position: Position,
in context: (ScaleFactorProviding & LayoutDirectionProviding)
in scale: CGFloat,
layoutDirection: UIUserInterfaceLayoutDirection
) -> CGRect {
return self.rect(
toFit: rect,
CGRect(
size: size(toFit: rect.size, in: scale),
at: position,
in: context,
layoutDirection: context.effectiveUserInterfaceLayoutDirection
of: rect,
in: scale,
layoutDirection: layoutDirection
)
}

Expand All @@ -221,15 +270,29 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - returns: A size with the receiver's aspect ratio, at least as large as the bounding size.
@MainActor
public func size(toFill size: CGSize, in scaleFactor: ScaleFactorProviding) -> CGSize {
self.size(toFill: size, in: scaleFactor.pixelsPerPoint)
}

/// An "aspect-fill" function that determines the smallest size of the receiver's aspect ratio that fits a size
/// within it.
///
/// - parameter size: The bounding size.
/// - parameter scale: The number of pixels per point.
/// - returns: A size with the receiver's aspect ratio, at least as large as the bounding size.
public func size(toFill size: CGSize, in scale: CGFloat) -> CGSize {
if size.aspectRatio <= self {
// Match height, expand the width.
let fillWidth = width(forHeight: size.height, in: scaleFactor)
return CGSize(width: fillWidth, height: size.height)
CGSize(
width: width(forHeight: size.height, in: scale),
height: size.height
)

} else {
// Match width, expand the height.
let fillHeight = height(forWidth: size.width, in: scaleFactor)
return CGSize(width: size.width, height: fillHeight)
CGSize(
width: size.width,
height: height(forWidth: size.width, in: scale)
)
}
}

Expand All @@ -249,14 +312,39 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
in scaleFactor: ScaleFactorProviding,
layoutDirection: UIUserInterfaceLayoutDirection
) -> CGRect {
return CGRect(
CGRect(
size: size(toFill: rect.size, in: scaleFactor),
at: position,
of: rect,
in: scaleFactor,
layoutDirection: layoutDirection
)
}

/// An "aspect-fill" function that determines the smallest rect of the receiver's aspect ratio that fits a rect
/// within it.
///
/// - parameter rect: The bounding rect.
/// - parameter position: The location within the bounding rect for the new rect, determining where margin(s) will
/// be if the aspect ratios do not match perfectly.
/// - parameter scale: The number of pixels per point.
/// - parameter layoutDirection: The effective layout direction of the view in which the `rect` is defined.
/// - returns: A rect with the receiver's aspect ratio, strictly containing the bounding rect.
public func rect(
toFill rect: CGRect,
at position: Position,
in scale: CGFloat,
layoutDirection: UIUserInterfaceLayoutDirection
) -> CGRect {
CGRect(
size: size(toFill: rect.size, in: scale),
at: position,
of: rect,
in: scale,
layoutDirection: layoutDirection
)
}

/// An "aspect-fill" function that determines the smallest rect of the receiver's aspect ratio that fits a rect
/// within it.
///
Expand All @@ -272,7 +360,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
at position: Position,
in context: (ScaleFactorProviding & LayoutDirectionProviding)
) -> CGRect {
return self.rect(
self.rect(
toFill: rect,
at: position,
in: context,
Expand All @@ -288,7 +376,7 @@ extension CGSize {

/// The aspect ratio of this size.
public var aspectRatio: AspectRatio {
return AspectRatio(size: self)
AspectRatio(size: self)
}

}
Expand All @@ -299,7 +387,7 @@ extension CGRect {

/// The aspect ratio of this rect's size.
public var aspectRatio: AspectRatio {
return AspectRatio(size: size)
AspectRatio(size: size)
}

// MARK: - Life Cycle
Expand All @@ -311,6 +399,22 @@ extension CGRect {
of alignmentRect: CGRect,
in scaleFactor: ScaleFactorProviding,
layoutDirection: UIUserInterfaceLayoutDirection
) {
self.init(
size: newSize,
at: position,
of: alignmentRect,
in: scaleFactor.pixelsPerPoint,
layoutDirection: layoutDirection
)
}

fileprivate init(
size newSize: CGSize,
at position: Position,
of alignmentRect: CGRect,
in scale: CGFloat,
layoutDirection: UIUserInterfaceLayoutDirection
) {
let newOrigin: CGPoint

Expand All @@ -321,7 +425,7 @@ extension CGRect {
case .topLeft, .topCenter, .topRight:
newMinY = alignmentRect.minY
case .leftCenter, .center, .rightCenter:
newMinY = (alignmentRect.midY - newSize.height / 2).roundedToPixel(in: scaleFactor)
newMinY = (alignmentRect.midY - newSize.height / 2).roundedToPixel(in: scale)
case .bottomLeft, .bottomCenter, .bottomRight:
newMinY = alignmentRect.maxY - newSize.height
}
Expand All @@ -335,7 +439,7 @@ extension CGRect {
case .topLeft, .leftCenter, .bottomLeft:
newMinX = alignmentRect.minX
case .topCenter, .center, .bottomCenter:
newMinX = (alignmentRect.midX - newSize.width / 2).roundedToPixel(in: scaleFactor)
newMinX = (alignmentRect.midX - newSize.width / 2).roundedToPixel(in: scale)
case .topRight, .rightCenter, .bottomRight:
newMinX = alignmentRect.maxX - newSize.width
}
Expand Down
Loading