Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// ScaleEffectUITests.swift
// OpenSwiftUIUITests

import Testing
import SnapshotTesting

@MainActor
@Suite(.snapshots(record: .never, diffTool: diffTool))
struct ScaleEffectUITests {
@Test
func scaleWithFrame() {
struct ContentView: View {
var body: some View {
Color.blue
.frame(width: 80, height: 60)
.scaleEffect(0.5)
.background { Color.red }
.overlay {
Color.green
.frame(width: 40, height: 30)
.scaleEffect(1.5)
}
}
}
openSwiftUIAssertSnapshot(of: ContentView())
}

@Test
func scaleWithAnchor() {
struct ContentView: View {
var body: some View {
Color.blue
.frame(width: 80, height: 60)
.scaleEffect(0.5, anchor: .topLeading)
.background { Color.red }
}
}
openSwiftUIAssertSnapshot(of: ContentView())
}

@Test
func scaleWithXY() {
struct ContentView: View {
var body: some View {
Color.blue
.frame(width: 80, height: 60)
.scaleEffect(x: 0.5, y: 1.5)
.background { Color.red }
}
}
openSwiftUIAssertSnapshot(of: ContentView())
}

@Test
func scaleToZero() {
struct ContentView: View {
var body: some View {
Color.blue
.frame(width: 80, height: 60)
.scaleEffect(0)
.background { Color.red }
}
}
openSwiftUIAssertSnapshot(of: ContentView())
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ public struct ContentTransition: Equatable, Sendable {
}
}


@_spi(Private)
public struct EffectType: Equatable, Sendable {
package enum Arg: Equatable, Sendable {
Expand Down
241 changes: 241 additions & 0 deletions Sources/OpenSwiftUICore/Render/GeometryEffect/ScaleEffect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
//
// ScaleEffect.swift
// OpenSwiftUICore
//
// Audited for 6.5.4
// Status: Complete
// ID: 8AD2EA4DF9F96B2E7AB78754CF15EB14 (SwiftUICore)

public import OpenCoreGraphicsShims

private let leastNonzeroScaleFactor = (2 * CGFloat.leastNormalMagnitude).squareRoot()

// MARK: - ScaleEffect

@available(OpenSwiftUI_v1_0, *)
@frozen
public struct _ScaleEffect: GeometryEffect, Equatable {

public var scale: CGSize

public var anchor: UnitPoint

@inlinable
public init(scale: CGSize, anchor: UnitPoint = .center) {
self.scale = scale
self.anchor = anchor
}

public func effectValue(size: CGSize) -> ProjectionTransform {
var effectScale = scale
if scale.width == 0 {
effectScale.width = leastNonzeroScaleFactor
}
if scale.height == 0 {
effectScale.height = leastNonzeroScaleFactor
}
let position = anchor.in(size)
let negatePosition = -anchor.in(size)
let transform = CGAffineTransform(translationX: negatePosition.x, y: negatePosition.y)
.concatenating(.init(scaleX: effectScale.width, y: effectScale.height))
.concatenating(.init(translationX: position.x, y: position.y))
return ProjectionTransform(transform)
}

public typealias AnimatableData = AnimatablePair<CGSize.AnimatableData, UnitPoint.AnimatableData>

public var animatableData: AnimatableData {
get {
.init(scale.animatableData, anchor.animatableData)
}
set {
scale.animatableData = newValue.first
anchor.animatableData = newValue.second
}
}

nonisolated public static func _makeView(
modifier: _GraphValue<_ScaleEffect>,
inputs: _ViewInputs,
body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs
) -> _ViewOutputs {
makeGeometryEffect(modifier: modifier, inputs: inputs, body: body)
}
}

// MARK: - View + scaleEffect

@available(OpenSwiftUI_v1_0, *)
extension View {

/// Scales this view's rendered output by the given vertical and horizontal
/// size amounts, relative to an anchor point.
///
/// Use `scaleEffect(_:anchor:)` to scale a view by applying a scaling
/// transform of a specific size, specified by `scale`.
///
/// Image(systemName: "envelope.badge.fill")
/// .resizable()
/// .frame(width: 100, height: 100, alignment: .center)
/// .foregroundColor(Color.red)
/// .scaleEffect(CGSize(x: 0.9, y: 1.3), anchor: .leading)
/// .border(Color.gray)
///
/// ![A screenshot showing a red envelope scaled to a size of 90x130
/// pixels.](OpenSwiftUI-View-scaleEffect.png)
///
/// - Parameters:
/// - scale: A [CGSize](https://developer.apple.com/documentation/coregraphics/cgsize) that
/// represents the horizontal and vertical amount to scale the view.
/// - anchor: The point with a default of ``UnitPoint/center`` that
/// defines the location within the view from which to apply the
/// transformation.
@inlinable
nonisolated public func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some View {
return modifier(_ScaleEffect(scale: scale, anchor: anchor))
}

/// Scales this view's rendered output by the given amount in both the
/// horizontal and vertical directions, relative to an anchor point.
///
/// Use `scaleEffect(_:anchor:)` to apply a horizontally and vertically
/// scaling transform to a view.
///
/// Image(systemName: "envelope.badge.fill")
/// .resizable()
/// .frame(width: 100, height: 100, alignment: .center)
/// .foregroundColor(Color.red)
/// .scaleEffect(2, anchor: .leading)
/// .border(Color.gray)
///
/// ![A screenshot showing a 100x100 pixel red envelope scaled up to 2x the
/// size of its view.](OpenSwiftUI-View-scaleEffect-cgfloat.png)
///
/// - Parameters:
/// - s: The amount to scale the view in the view in both the horizontal
/// and vertical directions.
/// - anchor: The anchor point with a default of ``UnitPoint/center`` that
/// indicates the starting position for the scale operation.
@inlinable
nonisolated public func scaleEffect(_ s: CGFloat, anchor: UnitPoint = .center) -> some View {
return scaleEffect(CGSize(width: s, height: s), anchor: anchor)
}

/// Scales this view's rendered output by the given horizontal and vertical
/// amounts, relative to an anchor point.
///
/// Use `scaleEffect(x:y:anchor:)` to apply a scaling transform to a view by
/// a specific horizontal and vertical amount.
///
/// Image(systemName: "envelope.badge.fill")
/// .resizable()
/// .frame(width: 100, height: 100, alignment: .center)
/// .foregroundColor(Color.red)
/// .scaleEffect(x: 0.5, y: 0.5, anchor: .bottomTrailing)
/// .border(Color.gray)
///
/// ![A screenshot showing a 100x100 pixel red envelope scaled down 50% in
/// both the x and y axes.](OpenSwiftUI-View-scaleEffect-xy.png)
///
/// - Parameters:
/// - x: An amount that represents the horizontal amount to scale the
/// view. The default value is `1.0`.
/// - y: An amount that represents the vertical amount to scale the view.
/// The default value is `1.0`.
/// - anchor: The anchor point that indicates the starting position for
/// the scale operation.
@inlinable
nonisolated public func scaleEffect(x: CGFloat = 1.0, y: CGFloat = 1.0, anchor: UnitPoint = .center) -> some View {
return scaleEffect(CGSize(width: x, height: y), anchor: anchor)
}
}

// MARK: - AnyTransition + ScaleTransition

@available(OpenSwiftUI_v1_0, *)
extension AnyTransition {

/// Returns a transition that scales the view.
public static var scale: AnyTransition {
AnyTransition(ScaleTransition(1e-5))
}

/// Returns a transition that scales the view by the specified amount.
public static func scale(scale: CGFloat, anchor: UnitPoint = .center) -> AnyTransition {
AnyTransition(ScaleTransition(Double(scale), anchor: anchor))
}
}

// MARK: - Transition + ScaleTransition

@available(OpenSwiftUI_v5_0, *)
extension Transition where Self == ScaleTransition {

/// Returns a transition that scales the view.
@_alwaysEmitIntoClient
public static var scale: ScaleTransition {
get { Self(1e-5) }
}

/// Returns a transition that scales the view by the specified amount.
@_alwaysEmitIntoClient
public static func scale(_ scale: Double, anchor: UnitPoint = .center) -> Self {
Self(scale, anchor: anchor)
}
}

// MARK: - ScaleTransition

/// Returns a transition that scales the view.
@available(OpenSwiftUI_v5_0, *)
public struct ScaleTransition: Transition {

/// The amount to scale the view by.
public var scale: Double

/// The anchor point to scale the view around.
public var anchor: UnitPoint

/// Creates a transition that scales the view by the specified amount.
public init(_ scale: Double, anchor: UnitPoint = .center) {
self.scale = scale
self.anchor = anchor
}

public func body(content: ScaleTransition.Content, phase: TransitionPhase) -> some View {
content.scaleEffect(phase.isIdentity ? 1.0 : scale, anchor: anchor)
}

public func _makeContentTransition(transition: inout _Transition_ContentTransition) {
guard case .effects = transition.operation else {
transition.result = .bool(true)
return
}
let effect = ContentTransition.Effect(.scale(scale))
transition.result = .effects([effect])
}
}

@available(*, unavailable)
extension ScaleTransition: Sendable {}

// MARK: _ ScaleEffect + ProtobufMessage

extension _ScaleEffect: ProtobufMessage {
package func encode(to encoder: inout ProtobufEncoder) throws {
try encoder.messageField(1, scale, defaultValue: CGSize(width: 1, height: 1))
try encoder.messageField(2, anchor, defaultValue: .center)
}

package init(from decoder: inout ProtobufDecoder) throws {
var effect = _ScaleEffect(scale: CGSize(width: 1, height: 1), anchor: .center)
while let field = try decoder.nextField() {
switch field.tag {
case 1: effect.scale = try decoder.messageField(field)
case 2: effect.anchor = try decoder.messageField(field)
default: try decoder.skipField(field)
}
}
self = effect
}
}
Loading