Skip to content

Commit

Permalink
Create non-@MainActor-bound methods for rounding to pixel at scale
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed committed Oct 30, 2024
1 parent f8489e6 commit 1d74c6b
Showing 1 changed file with 116 additions and 24 deletions.
140 changes: 116 additions & 24 deletions Paralayout/PixelRounding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public protocol ScaleFactorProviding {
extension UIScreen: ScaleFactorProviding {

public var pixelsPerPoint: CGFloat {
return scale
scale
}

}
Expand All @@ -53,15 +53,15 @@ extension UIView: ScaleFactorProviding {
extension CGFloat: ScaleFactorProviding {

public var pixelsPerPoint: CGFloat {
return self
self
}

}

extension Int: ScaleFactorProviding {

public var pixelsPerPoint: CGFloat {
return CGFloat(self)
CGFloat(self)
}

}
Expand All @@ -79,7 +79,15 @@ extension CGFloat {
/// - returns: The adjusted coordinate.
@MainActor
public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
return adjustedToPixel(scaleFactor) { floor($0) }
flooredToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the coordinate value (in points) floored to the nearest pixel, e.g. 0.6 @2x -> 0.5, not 0.0.
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func flooredToPixel(in scale: CGFloat) -> CGFloat {
adjustedToPixel(scale) { floor($0) }
}

/// Returns the coordinate value (in points) ceiled to the nearest pixel, e.g. 0.4 @2x -> 0.5, not 1.0.
Expand All @@ -89,7 +97,15 @@ extension CGFloat {
/// - returns: The adjusted coordinate.
@MainActor
public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
return adjustedToPixel(scaleFactor) { ceil($0) }
ceiledToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the coordinate value (in points) ceiled to the nearest pixel, e.g. 0.4 @2x -> 0.5, not 1.0.
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func ceiledToPixel(in scale: CGFloat) -> CGFloat {
adjustedToPixel(scale) { ceil($0) }
}

/// Returns the coordinate value (in points) rounded to the nearest pixel, e.g. 0.4 @2x -> 0.5, not 0.0.
Expand All @@ -99,19 +115,29 @@ extension CGFloat {
/// - returns: The adjusted coordinate.
@MainActor
public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
roundedToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the coordinate value (in points) rounded to the nearest pixel, e.g. 0.4 @2x -> 0.5, not 0.0.
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func roundedToPixel(in scale: CGFloat) -> CGFloat {
// Invoke the namespaced Darwin.round() function since round() is ambiguous (it's also a mutating instance
// method).
return adjustedToPixel(scaleFactor) { Darwin.round($0) }
adjustedToPixel(scale) { Darwin.round($0) }
}

// MARK: - Private Methods

@MainActor
private func adjustedToPixel(_ scaleFactor: ScaleFactorProviding, _ adjustment: (CGFloat) -> CGFloat) -> CGFloat {
let scale = scaleFactor.pixelsPerPoint
return (scale > 0.0) ? (adjustment(self * scale) / scale) : self
adjustedToPixel(scaleFactor.pixelsPerPoint, adjustment)
}

private func adjustedToPixel(_ scale: CGFloat, _ adjustment: (CGFloat) -> CGFloat) -> CGFloat {
(scale > 0.0) ? (adjustment(self * scale) / scale) : self
}
}

extension CGPoint {
Expand All @@ -124,7 +150,16 @@ extension CGPoint {
/// - returns: The adjusted coordinate.
@MainActor
public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
return CGPoint(x: x.flooredToPixel(in: scaleFactor), y: y.flooredToPixel(in: scaleFactor))
flooredToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the coordinate values (in points) floored to the nearest pixel, e.g. (0.6, 1.1) @2x -> (0.5, 1.0), not
/// (0.0, 1.0).
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func flooredToPixel(in scale: CGFloat) -> CGPoint {
CGPoint(x: x.flooredToPixel(in: scale), y: y.flooredToPixel(in: scale))
}

/// Returns the coordinate values (in points) ceiled to the nearest pixel, e.g. (0.4, 1.1) @2x -> (0.5, 1.5), not
Expand All @@ -135,7 +170,16 @@ extension CGPoint {
/// - returns: The adjusted coordinate.
@MainActor
public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
return CGPoint(x: x.ceiledToPixel(in: scaleFactor), y: y.ceiledToPixel(in: scaleFactor))
ceiledToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the coordinate values (in points) ceiled to the nearest pixel, e.g. (0.4, 1.1) @2x -> (0.5, 1.5), not
/// (1.0, 2.0).
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func ceiledToPixel(in scale: CGFloat) -> CGPoint {
CGPoint(x: x.ceiledToPixel(in: scale), y: y.ceiledToPixel(in: scale))
}

/// Returns the coordinate values (in points) rounded to the nearest pixel, e.g. (0.4, 0.5) @2x -> (0.5, 0.5), not
Expand All @@ -146,9 +190,17 @@ extension CGPoint {
/// - returns: The adjusted coordinate.
@MainActor
public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
return CGPoint(x: x.roundedToPixel(in: scaleFactor), y: y.roundedToPixel(in: scaleFactor))
roundedToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the coordinate values (in points) rounded to the nearest pixel, e.g. (0.4, 0.5) @2x -> (0.5, 0.5), not
/// (0.0, 1.0).
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func roundedToPixel(in scale: CGFloat) -> CGPoint {
CGPoint(x: x.roundedToPixel(in: scale), y: y.roundedToPixel(in: scale))
}
}

extension CGSize {
Expand All @@ -160,7 +212,15 @@ extension CGSize {
/// - returns: The adjusted coordinate.
@MainActor
public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(width: width.flooredToPixel(in: scaleFactor), height: height.flooredToPixel(in: scaleFactor))
flooredToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Return the size (in points) floored to the nearest pixel, e.g. (0.6, 1.1) @2x -> (0.5, 1.0), not (0.0, 1.0).
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func flooredToPixel(in scale: CGFloat) -> CGSize {
CGSize(width: width.flooredToPixel(in: scale), height: height.flooredToPixel(in: scale))
}

/// Returns the size (in points) ceiled to the nearest pixel, e.g. (0.4, 1.1) @2x -> (0.5, 1.5), not (1.0, 2.0)).
Expand All @@ -170,7 +230,15 @@ extension CGSize {
/// - returns: The adjusted coordinate.
@MainActor
public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(width: width.ceiledToPixel(in: scaleFactor), height: height.ceiledToPixel(in: scaleFactor))
ceiledToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the size (in points) ceiled to the nearest pixel, e.g. (0.4, 1.1) @2x -> (0.5, 1.5), not (1.0, 2.0)).
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func ceiledToPixel(in scale: CGFloat) -> CGSize {
CGSize(width: width.ceiledToPixel(in: scale), height: height.ceiledToPixel(in: scale))
}

/// Returns the size (in points) rounded to the nearest pixel, e.g. (0.4, 0.5) @2x -> (0.5, 0.5), not (0.0, 1.0).
Expand All @@ -180,7 +248,15 @@ extension CGSize {
/// - returns: The adjusted coordinate.
@MainActor
public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(width: width.roundedToPixel(in: scaleFactor), height: height.roundedToPixel(in: scaleFactor))
roundedToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the size (in points) rounded to the nearest pixel, e.g. (0.4, 0.5) @2x -> (0.5, 0.5), not (0.0, 1.0).
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: The adjusted coordinate.
public func roundedToPixel(in scale: CGFloat) -> CGSize {
CGSize(width: width.roundedToPixel(in: scale), height: height.roundedToPixel(in: scale))
}

}
Expand All @@ -194,11 +270,19 @@ extension CGRect {
/// - returns: A new rect with pixel-aligned boundaries, enclosing the original rect.
@MainActor
public func expandedToPixel(in scaleFactor: ScaleFactorProviding) -> CGRect {
return CGRect(
left: minX.flooredToPixel(in: scaleFactor),
top: minY.flooredToPixel(in: scaleFactor),
right: maxX.ceiledToPixel(in: scaleFactor),
bottom: maxY.ceiledToPixel(in: scaleFactor)
expandedToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the rect, outset if necessary to align each edge to the nearest pixel at the specified scale.
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: A new rect with pixel-aligned boundaries, enclosing the original rect.
public func expandedToPixel(in scale: CGFloat) -> CGRect {
CGRect(
left: minX.flooredToPixel(in: scale),
top: minY.flooredToPixel(in: scale),
right: maxX.ceiledToPixel(in: scale),
bottom: maxY.ceiledToPixel(in: scale)
)
}

Expand All @@ -209,11 +293,19 @@ extension CGRect {
/// - returns: A new rect with pixel-aligned boundaries, enclosed by the original rect.
@MainActor
public func contractedToPixel(in scaleFactor: ScaleFactorProviding) -> CGRect {
return CGRect(
left: minX.ceiledToPixel(in: scaleFactor),
top: minY.ceiledToPixel(in: scaleFactor),
right: maxX.flooredToPixel(in: scaleFactor),
bottom: maxY.flooredToPixel(in: scaleFactor)
contractedToPixel(in: scaleFactor.pixelsPerPoint)
}

/// Returns the rect, inset if necessary to align each edge to the nearest pixel at the specified scale.
///
/// - parameter scale: The pixel scale to use (pass `0` to *not* snap to pixel).
/// - returns: A new rect with pixel-aligned boundaries, enclosed by the original rect.
public func contractedToPixel(in scale: CGFloat) -> CGRect {
CGRect(
left: minX.ceiledToPixel(in: scale),
top: minY.ceiledToPixel(in: scale),
right: maxX.flooredToPixel(in: scale),
bottom: maxY.flooredToPixel(in: scale)
)
}

Expand Down

0 comments on commit 1d74c6b

Please sign in to comment.