diff --git a/Example/HostingExample/ViewController.swift b/Example/HostingExample/ViewController.swift index e5605f660..18e904fce 100644 --- a/Example/HostingExample/ViewController.swift +++ b/Example/HostingExample/ViewController.swift @@ -66,6 +66,6 @@ class ViewController: NSViewController { struct ContentView: View { var body: some View { - DynamicLayoutViewExample() + MyViewThatFitsExample() } } diff --git a/Example/SharedExample/Layout/FlowLayout.swift b/Example/SharedExample/Layout/CustomLayout/FlowLayout.swift similarity index 100% rename from Example/SharedExample/Layout/FlowLayout.swift rename to Example/SharedExample/Layout/CustomLayout/FlowLayout.swift diff --git a/Example/SharedExample/Layout/MyViewThatFitsByLayout.swift b/Example/SharedExample/Layout/CustomLayout/MyViewThatFitsByLayout.swift similarity index 100% rename from Example/SharedExample/Layout/MyViewThatFitsByLayout.swift rename to Example/SharedExample/Layout/CustomLayout/MyViewThatFitsByLayout.swift diff --git a/Example/SharedExample/Layout/CustomLayout/MyViewThatFitsExample.swift b/Example/SharedExample/Layout/CustomLayout/MyViewThatFitsExample.swift new file mode 100644 index 000000000..7d81adf2f --- /dev/null +++ b/Example/SharedExample/Layout/CustomLayout/MyViewThatFitsExample.swift @@ -0,0 +1,28 @@ +// +// MyViewThatFitsExample.swift +// SharedExample + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +// The DL does not fully align since we have not implement canonicalize API yet. +// See https://github.com/OpenSwiftUIProject/OpenSwiftUI/issues/349 +struct MyViewThatFitsExample: View { + @State private var showRed = false + var body: some View { + MyViewThatFitsByLayout { + Color.red.frame(width: 100, height: 200) + Color.blue.frame(width: 200, height: 100) + } + .frame(width: 100, height: showRed ? 200 : 100) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + showRed.toggle() + } + } + .id(showRed) + } +} diff --git a/Example/SharedExample/Layout/MyZStackLayout.swift b/Example/SharedExample/Layout/CustomLayout/MyZStackLayout.swift similarity index 100% rename from Example/SharedExample/Layout/MyZStackLayout.swift rename to Example/SharedExample/Layout/CustomLayout/MyZStackLayout.swift diff --git a/Sources/OpenSwiftUICore/Animation/Animatable/Animatable.swift b/Sources/OpenSwiftUICore/Animation/Animatable/Animatable.swift index a305efb85..0b226b026 100644 --- a/Sources/OpenSwiftUICore/Animation/Animatable/Animatable.swift +++ b/Sources/OpenSwiftUICore/Animation/Animatable/Animatable.swift @@ -11,6 +11,7 @@ package import OpenAttributeGraphShims // MARK: - Animatable /// A type that describes how to animate a property of a view. +@available(OpenSwiftUI_v1_0, *) public protocol Animatable { /// The type defining the data to animate. associatedtype AnimatableData: VectorArithmetic @@ -25,6 +26,7 @@ public protocol Animatable { // MARK: - Animateble + Extension +@available(OpenSwiftUI_v1_0, *) extension Animatable where Self: VectorArithmetic { public var animatableData: Self { get { self } @@ -32,6 +34,7 @@ extension Animatable where Self: VectorArithmetic { } } +@available(OpenSwiftUI_v1_0, *) extension Animatable where AnimatableData == EmptyAnimatableData { public var animatableData: EmptyAnimatableData { @inlinable @@ -59,6 +62,7 @@ extension Attribute where Value: Animatable { } } +@available(OpenSwiftUI_v1_0, *) extension Animatable { public static func _makeAnimatable(value: inout _GraphValue, inputs: _GraphInputs) { guard MemoryLayout.size != 0, @@ -84,6 +88,7 @@ extension Animatable { /// /// This type is suitable for use as the `animatableData` property of /// types that do not have any animatable properties. +@available(OpenSwiftUI_v1_0, *) @frozen public struct EmptyAnimatableData: VectorArithmetic { @inlinable @@ -113,10 +118,12 @@ public struct EmptyAnimatableData: VectorArithmetic { public static func == (_: EmptyAnimatableData, _: EmptyAnimatableData) -> Bool { true } } +@available(OpenSwiftUI_v5_0, *) extension Double: Animatable { public typealias AnimatableData = Double } +@available(OpenSwiftUI_v5_0, *) extension CGFloat: Animatable { public typealias AnimatableData = CGFloat } diff --git a/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift b/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift index b21d16a7e..e9bb03d5c 100644 --- a/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift +++ b/Sources/OpenSwiftUICore/Animation/Transition/ContentTransition.swift @@ -4,6 +4,9 @@ // TODO +public import OpenCoreGraphicsShims +package import OpenRenderBoxShims + @available(OpenSwiftUI_v4_0, *) public struct ContentTransition: Equatable, Sendable { package enum Storage: Equatable, @unchecked Sendable { @@ -72,8 +75,199 @@ public struct ContentTransition: Equatable, Sendable { // TODO: NumericTextConfiguration + @_spi(Private) + public struct EffectType: Equatable, Sendable { + package enum Arg: Equatable, Sendable { + case none + case float(Float) + case int(UInt32) + } + + package var type: ORBTransitionEffectType + package var arg0: ContentTransition.EffectType.Arg, arg1: ContentTransition.EffectType.Arg + + package init( + type: ORBTransitionEffectType, + arg0: ContentTransition.EffectType.Arg = .none, + arg1: ContentTransition.EffectType.Arg = .none + ) { + self.type = type + self.arg0 = arg0 + self.arg1 = arg1 + } + + public static var opacity: ContentTransition.EffectType { + self.init(type: .opacity, arg0: .none, arg1: .none) + } + + @available(*, deprecated, message: "use opacity variable") + public static func opacity(_ opacity: Double = 0) -> ContentTransition.EffectType { + self.init(type: .opacity, arg0: .none, arg1: .none) + } + + public static func blur(radius: CGFloat) -> ContentTransition.EffectType { + self.init(type: .blur, arg0: .float(Float(radius)), arg1: .none) + } + + @available(OpenSwiftUI_v6_0, *) + public static func relativeBlur(scale: CGSize) -> ContentTransition.EffectType { + self.init(type: .relativeBlur, arg0: .float(Float(scale.width)), arg1: .float(Float(scale.height))) + } + + public static func scale(_ scale: CGFloat = 0) -> ContentTransition.EffectType { + self.init(type: .scale, arg0: .float(Float(scale)), arg1: .none) + } + + public static func translation(_ size: CGSize) -> ContentTransition.EffectType { + self.init(type: .translationSize, arg0: .float(Float(size.width)), arg1: .float(Float(size.height))) + } + + @available(OpenSwiftUI_v6_0, *) + public static func translation(scale: CGSize) -> ContentTransition.EffectType { + self.init(type: .translationScale, arg0: .float(Float(scale.width)), arg1: .float(Float(scale.height))) + } + + public static var matchMove: ContentTransition.EffectType { + self.init(type: .matchMove, arg0: .none, arg1: .none) + } + } + + @_spi(Private) + @available(OpenSwiftUI_v6_0, *) + public enum SequenceDirection: Hashable, Sendable { + case leading, trailing, up, down + case forwards, backwards + } + + @_spi(Private) + public struct Effect: Equatable, Sendable { + package var type: ContentTransition.EffectType + package var begin: Float + package var duration: Float + package var events: ORBTransitionEvents + package var flags: ORBTransitionEffectFlags + + package init( + type: ContentTransition.EffectType, + begin: Float = 0, + duration: Float = 1, + events: ORBTransitionEvents = .addRemove, + flags: ORBTransitionEffectFlags = .init() + ) { + self.type = type + self.begin = begin + self.duration = duration + self.events = events + self.flags = flags + } + + public init( + _ type: ContentTransition.EffectType, + timeline: ClosedRange = 0 ... 1, + appliesOnInsertion: Bool = true, + appliesOnRemoval: Bool = true + ) { + self.type = type + self.begin = timeline.lowerBound + self.duration = timeline.upperBound - timeline.lowerBound + self.events = [ + appliesOnInsertion ? .add : [], + appliesOnRemoval ? .remove : [], + ] + self.flags = [] + } + + @available(OpenSwiftUI_v6_0, *) + public static func sequence( + direction: ContentTransition.SequenceDirection, + delay: Double, + maxAllowedDurationMultiple: Double = .infinity, + appliesOnInsertion: Bool = true, + appliesOnRemoval: Bool = true + ) -> ContentTransition.Effect { + _openSwiftUIUnimplementedFailure() + } + + @available(OpenSwiftUI_v6_0, *) + public func removeInverts(_ state: Bool) -> ContentTransition.Effect { + _openSwiftUIUnimplementedFailure() + } + } + + @_spi(Private) + public struct Method: Equatable, Sendable { + package var method: ORBTransitionMethod + + package init(method: ORBTransitionMethod) { + self.method = method + } + + public static let diff: ContentTransition.Method = .init(method: .diff) + + public static let forwards: ContentTransition.Method = .init(method: .forwards) + + public static let backwards: ContentTransition.Method = .init(method: .backwards) + + public static let prefix: ContentTransition.Method = .init(method: .prefix) + + public static let suffix: ContentTransition.Method = .init(method: .suffix) + + public static let prefixAndSuffix: ContentTransition.Method = .init(method: .prefixAndSuffix) + + public static let binary: ContentTransition.Method = .init(method: .binary) + + public static let none: ContentTransition.Method = .init(method: .none) + + public static func == (a: ContentTransition.Method, b: ContentTransition.Method) -> Bool { + a.method == b.method + } + } + // TODO - package enum Effect {} + package struct State {} +} + +// FIXME: ORB + +package enum ORBTransitionMethod: Int { + case empty + case diff + case forwards + case backwards + case prefix + case suffix + case binary + case none + case prefixAndSuffix +} + +// ProtobufEnum +package struct ORBTransitionEvents: OptionSet { + package let rawValue: UInt32 + + package init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let add: ORBTransitionEvents = .init(rawValue: 1) + public static let remove: ORBTransitionEvents = .init(rawValue: 2) + public static let addRemove: ORBTransitionEvents = .init(rawValue: 3) +} + +package struct ORBTransitionEffectFlags: OptionSet { + package let rawValue: UInt32 + + package init(rawValue: UInt32) { + self.rawValue = rawValue + } +} - package enum State {} +package enum ORBTransitionEffectType: UInt32, Equatable { + case opacity = 1 + case scale = 2 + case translationSize = 3 + case blur = 4 + case matchMove = 5 + case translationScale = 15 + case relativeBlur = 16 } diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index cd22674d5..02edecaee 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -477,7 +477,7 @@ extension PreferencesOutputs { extension DisplayList.Item { package mutating func canonicalize(options: DisplayList.Options = .init()) { - // TODO + // TODO eg. .opacity(1.0) -> .identity } // package func matchesTopLevelStructure(of other: DisplayList.Item) -> Bool diff --git a/Sources/OpenSwiftUICore/Render/OpacityEffect.swift b/Sources/OpenSwiftUICore/Render/OpacityEffect.swift index edef3940f..8b78b0d64 100644 --- a/Sources/OpenSwiftUICore/Render/OpacityEffect.swift +++ b/Sources/OpenSwiftUICore/Render/OpacityEffect.swift @@ -1,21 +1,311 @@ // // OpacityEffect.swift // OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: 34FFA2034B9AD53E0463E3971529C5A1 (SwiftUICore) + +package import OpenCoreGraphicsShims +import OpenAttributeGraphShims + +// MARK: - _OpacityEffect + +@available(OpenSwiftUI_v1_0, *) +@frozen +@MainActor +@preconcurrency +public struct _OpacityEffect: RendererEffect, Equatable { + public var opacity: Double + + @inlinable + nonisolated public init(opacity: Double) { + self.opacity = opacity + } + + public var animatableData: Double { + get { opacity } + set { opacity = newValue } + } + + package func effectValue(size: CGSize) -> DisplayList.Effect { + .opacity(Float(opacity)) + } + + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + makeRendererEffect(effect: modifier, inputs: inputs) { graph, inputs in + var outputs = body(graph, inputs) + if inputs.preferences.requiresViewResponders { + let responder = OpacityViewResponder(inputs: inputs) + outputs.preferences.viewResponders = Attribute( + OpacityResponderFilter( + effect: modifier.value, + children: .init(outputs.preferences.viewResponders), + responder: responder + ) + ) + } + return outputs + } + } +} + +// MARK: - View + opacity +@available(OpenSwiftUI_v1_0, *) +extension View { + + /// Sets the transparency of this view. + /// + /// Apply opacity to reveal views that are behind another view or to + /// de-emphasize a view. + /// + /// When applying the `opacity(_:)` modifier to a view that has already had + /// its opacity transformed, the modifier multiplies the effect of the + /// underlying opacity transformation. + /// + /// The example below shows yellow and red rectangles configured to overlap. + /// The top yellow rectangle has its opacity set to 50%, allowing the + /// occluded portion of the bottom rectangle to be visible: + /// + /// struct Opacity: View { + /// var body: some View { + /// VStack { + /// Color.yellow.frame(width: 100, height: 100, alignment: .center) + /// .zIndex(1) + /// .opacity(0.5) + /// + /// Color.red.frame(width: 100, height: 100, alignment: .center) + /// .padding(-40) + /// } + /// } + /// } + /// + /// ![Two overlaid rectangles, where the topmost has its opacity set to 50%, + /// which allows the occluded portion of the bottom rectangle to be + /// visible.](OpenSwiftUI-View-opacity.png) + /// + /// - Parameter opacity: A value between 0 (fully transparent) and 1 (fully + /// opaque). + /// + /// - Returns: A view that sets the transparency of this view. + @inlinable + nonisolated public func opacity(_ opacity: Double) -> some View { + modifier(_OpacityEffect(opacity: opacity)) + } +} + +// MARK: OpacityRendererEffect + +@MainActor +@preconcurrency +package struct OpacityRendererEffect: RendererEffect { + package var opacity: Double + + package init(opacity: Double) { + self.opacity = opacity + } + + package init(isHidden: Bool) { + self.opacity = isHidden ? 0.0 : 1.0 + } + + package var animatableData: Double { + get { opacity } + set { opacity = newValue } + } + + package func effectValue(size: CGSize) -> DisplayList.Effect { + .opacity(Float(opacity)) + } + + nonisolated package static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + makeRendererEffect(effect: modifier, inputs: inputs, body: body) + } +} + +// MARK: - OpacityViewResponder + +private final class OpacityViewResponder: DefaultLayoutViewResponder { + var _opacity: Double + + override init(inputs: _ViewInputs) { + _opacity = 1.0 + super.init(inputs: inputs) + } + + override init(inputs: _ViewInputs, viewSubgraph: Subgraph) { + _opacity = 1.0 + super.init(inputs: inputs, viewSubgraph: viewSubgraph) + } + + override var opacity: Double { _opacity } + + override func containsGlobalPoints( + _ points: [PlatformPoint], + cacheKey: UInt32?, + options: ViewResponder.ContainsPointsOptions + ) -> ViewResponder.ContainsPointsResult { + guard _opacity > 0 else { + return .init(mask: [], priority: .zero, children: children) + } + return super.containsGlobalPoints(points, cacheKey: cacheKey, options: options) + } + + override func extendPrintTree(string: inout String) { + string.append("opacity \(_opacity)") + } +} + +// MARK: Transition + Opacity + +@available(OpenSwiftUI_v1_0, *) extension AnyTransition { - // FIXME + + /// A transition from transparent to opaque on insertion, and from opaque to + /// transparent on removal. public static let opacity: AnyTransition = .init(OpacityTransition()) } -extension View { - func opacity(_ value: Double) -> some View { - // FIXME - modifier(EmptyModifier()) +@available(OpenSwiftUI_v5_0, *) +extension Transition where Self == OpacityTransition { + + /// A transition from transparent to opaque on insertion, and from opaque to + /// transparent on removal. + @_alwaysEmitIntoClient + @MainActor + @preconcurrency + public static var opacity: OpacityTransition { + get { Self() } + } +} + +// MARK: _OpacityEffect + ProtobufMessage + +extension _OpacityEffect: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.floatField(1, Float(opacity), defaultValue: 1.0) + } + + package init(from decoder: inout ProtobufDecoder) throws { + var opacity = 1.0 + while let field = try decoder.nextField() { + switch field.tag { + case 1: opacity = Double(try decoder.floatField(field)) + default: try decoder.skipField(field) + } + } + self.init(opacity: opacity) + } +} + +// MARK: - ShapeStyle + _OpacityShapeStyle + +// TODO: _OpacityShapeStyle + +// TODO: _OpacitiesShapeStyle + +// MARK: - OpacityTransition + +/// A transition from transparent to opaque on insertion, and from opaque to +/// transparent on removal. +@available(OpenSwiftUI_v5_0, *) +public struct OpacityTransition: Transition { + public init() {} + + public func body( + content: OpacityTransition.Content, + phase: TransitionPhase + ) -> some View { + content.modifier( + OpacityRendererEffect(opacity: phase.isIdentity ? 1.0 : 0.0) + ) + } + + public static let properties: TransitionProperties = .init(hasMotion: false) + + public func _makeContentTransition( + transition: inout _Transition_ContentTransition + ) { + switch transition.operation { + case .hasContentTransition: + transition.result = .bool(false) + case .effects: + transition.result = .effects([.init(type: .opacity)]) + } + } +} + +@available(*, unavailable) +extension OpacityTransition: Sendable {} + +// MARK: - OpacityResponderFilter + +struct OpacityResponderFilter: StatefulRule { + @Attribute var effect: _OpacityEffect + @OptionalAttribute var children: [ViewResponder]? + fileprivate let responder: OpacityViewResponder + + typealias Value = [ViewResponder] + + func updateValue() { + responder._opacity = effect.opacity + if let (children, changed) = $children?.changedValue(), + changed { + responder.children = children + } + if !hasValue { + value = [responder] + } + } +} + +// MARK: - OpacityAccessibilityProvider + +protocol OpacityAccessibilityProvider { + static func makeOpacity( + effect: @autoclosure () -> Attribute<_OpacityEffect>, + inputs: _ViewInputs, + outputs: inout _ViewOutputs + ) +} + +// MARK: - EmptyOpacityAccessibilityProvider + +struct EmptyOpacityAccessibilityProvider: OpacityAccessibilityProvider { + static func makeOpacity( + effect: @autoclosure () -> Attribute<_OpacityEffect>, + inputs: _ViewInputs, + outputs: inout _ViewOutputs + ) { + _openSwiftUIEmptyStub() + } +} + +// MARK: - OpacityAccessibilityProviderKey + +extension _GraphInputs { + private struct OpacityAccessibilityProviderKey: GraphInput { + static let defaultValue: OpacityAccessibilityProvider.Type = EmptyOpacityAccessibilityProvider.self + } + + var opacityAccessibilityProvider: OpacityAccessibilityProvider.Type { + get { self[OpacityAccessibilityProviderKey.self] } + set { self[OpacityAccessibilityProviderKey.self] = newValue } } } -struct OpacityTransition: Transition { - func body(content: Content, phase: TransitionPhase) -> some View { - content.opacity(1) +extension _ViewInputs { + var opacityAccessibilityProvider: OpacityAccessibilityProvider.Type { + get { base.opacityAccessibilityProvider } + set { base.opacityAccessibilityProvider = newValue } } } diff --git a/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift b/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift index b843169f2..00a0d9707 100644 --- a/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift +++ b/Sources/OpenSwiftUICore/View/Graph/ViewGraph.swift @@ -385,7 +385,7 @@ extension ViewGraph { } package func displayList() -> (DisplayList, DisplayList.Version) { - _openSwiftUIUnimplementedFailure() + $rootDisplayList?.value ?? (.init(), .init()) } private func beginNextUpdate(at time: Time) { diff --git a/Tests/OpenSwiftUICoreTests/Render/DisplayList/DisplayListTests.swift b/Tests/OpenSwiftUICoreTests/Render/DisplayList/DisplayListTests.swift index ecf877fe0..d58ff17f4 100644 --- a/Tests/OpenSwiftUICoreTests/Render/DisplayList/DisplayListTests.swift +++ b/Tests/OpenSwiftUICoreTests/Render/DisplayList/DisplayListTests.swift @@ -2,12 +2,13 @@ // DisplayListTests.swift // OpenSwiftUICoreTests +import Foundation import OpenSwiftUICore import Testing +@MainActor struct DisplayListTests { @Test - @MainActor func version() { typealias Version = DisplayList.Version let v0 = Version() diff --git a/Tests/OpenSwiftUICoreTests/View/IDViewTests.swift b/Tests/OpenSwiftUICoreTests/View/IDViewTests.swift index 83134cbcb..8654d0467 100644 --- a/Tests/OpenSwiftUICoreTests/View/IDViewTests.swift +++ b/Tests/OpenSwiftUICoreTests/View/IDViewTests.swift @@ -2,9 +2,9 @@ // IDViewTests.swift // OpenSwiftUICoreTests -@testable import OpenSwiftUICore -import Testing import Foundation +import OpenSwiftUICore +import Testing @MainActor struct IDViewTests { @@ -15,4 +15,50 @@ struct IDViewTests { _ = empty.id(2) _ = empty.id(UUID()) } + + #if canImport(Darwin) + @Test + func idViewDisplayList() { + struct ContentView: View { + var body: some View { + VStack { + Color.red + Color.blue + }.id(1) + } + } + let graph = ViewGraph( + rootViewType: ContentView.self, + requestedOutputs: [.displayList] + ) + graph.instantiateOutputs() + graph.setRootView(ContentView()) + graph.setProposedSize(CGSize(width: 100, height: 100)) + let (displayList, _) = graph.displayList() + + let expectRegex = try! Regex(#""" + \(display-list + \(item #:identity \d+ #:version \d+ + \(frame \([^)]+\)\)\)\) + """#) + #expect(displayList.description.contains(expectRegex)) + withKnownIssue("Blocked by DisplayList.print and canonicalize") { + let expectRegex = try! Regex(#""" + \(display-list + \(item #:identity \d+ #:version \d+ + \(frame \([^)]+\)\) + \(effect + \(item #:identity \d+ #:version \d+ + \(frame \([^)]+\)\) + \(content-seed \d+\) + \(color #[0-9A-F]{8}\)\) + \(item #:identity \d+ #:version \d+ + \(frame \([^)]+\)\) + \(content-seed \d+\) + \(color #[0-9A-F]{8}\)\)\)\)\) + """#) + #expect(displayList.description.contains(expectRegex)) + } + } + #endif }