diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift index e9066e406..df0ef785e 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewRepresentable.swift @@ -669,21 +669,20 @@ private struct LeafLayoutEnvironment: StatefulRule { func updateValue() { let (env, envChanged) = $environment.changedValue() - let shouldReset: Bool + let changed: Bool if !hasValue { - shouldReset = true + changed = true } else if envChanged, tracker.hasDifferentUsedValues(env.plist) { - shouldReset = true + changed = true } else { - shouldReset = false - } - if shouldReset { - tracker.reset() - value = EnvironmentValues( - environment.plist, - tracker: tracker - ) + changed = false } + guard changed else { return } + tracker.reset() + value = EnvironmentValues( + environment.plist, + tracker: tracker + ) } } diff --git a/Sources/OpenSwiftUICore/Render/SymbolEffect.swift b/Sources/OpenSwiftUICore/Render/SymbolEffect.swift new file mode 100644 index 000000000..2bdab2b50 --- /dev/null +++ b/Sources/OpenSwiftUICore/Render/SymbolEffect.swift @@ -0,0 +1,49 @@ +// +// SymbolEffect.swift +// OpenSwiftUICore +// +// Status: Empty + +import OpenAttributeGraphShims + +package struct _SymbolEffect: Equatable { + +} + +extension _SymbolEffect { + package struct Identified: Equatable { + + } + + package struct Phase: Equatable { + package init() { + // TODO + } + } +} + +extension EnvironmentValues { + package var symbolEffects: [_SymbolEffect.Identified] { + get { _openSwiftUIUnimplementedFailure() } + set { _openSwiftUIUnimplementedFailure() } + } + + package mutating func appendSymbolEffect( + _ effect: _SymbolEffect, + for identifier: Int + ) { + _openSwiftUIUnimplementedFailure() + } +} + +extension GraphicsImage { + mutating func updateSymbolEffects( + _ phase: inout _SymbolEffect.Phase, + environment: EnvironmentValues, + transaction: Attribute, + animationsDisabled: Bool + ) -> ORBSymbolAnimator? { + _openSwiftUIUnimplementedWarning() + return nil + } +} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ForegroundStyle.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ForegroundStyle.swift index 965196c10..0bc111060 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ForegroundStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ForegroundStyle.swift @@ -6,6 +6,8 @@ // Status: Complete // ID: BEFE9363F68E039B4AB6422B8AA4535A (SwiftUICore) +package import OpenAttributeGraphShims + // MARK: - ForegroundStyleKey private struct ForegroundStyleKey: EnvironmentKey { @@ -40,6 +42,16 @@ extension EnvironmentValues { } } +extension CachedEnvironment.ID { + package static let foregroundStyle: CachedEnvironment.ID = .init() +} + +extension _ViewInputs { + package var foregroundStyle: Attribute { + mapEnvironment(id: .foregroundStyle) { $0.foregroundStyle } + } +} + @available(OpenSwiftUI_v1_0, *) extension ShapeStyle where Self == ForegroundStyle { /// The foreground style in the current context. diff --git a/Sources/OpenSwiftUICore/Shape/Tint.swift b/Sources/OpenSwiftUICore/Shape/Tint.swift index abf018e77..d2cb8a5eb 100644 --- a/Sources/OpenSwiftUICore/Shape/Tint.swift +++ b/Sources/OpenSwiftUICore/Shape/Tint.swift @@ -6,6 +6,8 @@ // Status: WIP // ID: EB037BD7690CB8A700384AACA7B075E4 (SwiftUICore) +package import OpenAttributeGraphShims + // MARK: - View + tint ShapeStyle @available(OpenSwiftUI_v4_0, *) @@ -174,4 +176,14 @@ extension EnvironmentValues { } } +extension CachedEnvironment.ID { + package static let tintColor: CachedEnvironment.ID = .init() +} + +extension _ViewInputs { + package var tintColor: Attribute { + mapEnvironment(id: .tintColor) { $0.tintColor } + } +} + // MARK: - TintShapeStyle [TODO] diff --git a/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift b/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift index a6ee3b6ba..504ae2be5 100644 --- a/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/GraphicsImage.swift @@ -169,6 +169,11 @@ package struct ResolvedVectorGlyph: Equatable { package var flipsRightToLeft: Bool { animator.flipsRightToLeft } + + package func isClear(styles: ShapeStyle.Pack) -> Bool { + _openSwiftUIUnimplementedWarning() + return false + } } extension GraphicsImage { diff --git a/Sources/OpenSwiftUICore/View/Image/Image.swift b/Sources/OpenSwiftUICore/View/Image/Image.swift index 47b9f143e..28e4f3d54 100644 --- a/Sources/OpenSwiftUICore/View/Image/Image.swift +++ b/Sources/OpenSwiftUICore/View/Image/Image.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: Blocked by Image + View +// Status: Complete // ID: BE2D783904D422377BBEBAC3C942583C (SwiftUICore) package import OpenAttributeGraphShims @@ -162,7 +162,7 @@ package protocol ImageProvider: Equatable { func resolveNamedImage(in context: ImageResolutionContext) -> Image.NamedResolved? } -// MARK: - Image + View [WIP] +// MARK: - Image + View package protocol ImageStyleProtocol { static func _makeImageView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs @@ -174,8 +174,179 @@ extension Image: View, UnaryView, PrimitiveView { package static let defaultValue: Stack = .empty } - nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { - _openSwiftUIUnimplementedFailure() + nonisolated public static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + var newInputs = inputs + guard let style = newInputs.popLast(Style.self) else { + let flags = inputs.archivedView.flags + var options: ImageResolutionContext.Options = [] + if flags.contains(.isArchived) { + options.formUnion([.isArchived, .preservesVectors]) + if flags.contains(.assetCatalogRefences) { + options.formUnion(.useCatalogReferences) + } + } + if inputs.base.animationsDisabled { + options.formUnion(.animationsDisabled) + } + if newInputs.usingGraphicsRenderer, !flags.contains(.isArchived) { + options.formUnion(.preservesVectors) + } + var outputs = _ViewOutputs() + makeImageViewChild( + newInputs.imageAccessibilityProvider, + image: view.value, + options: options, + inputs: inputs, + outputs: &outputs + ) + if let representation = inputs.requestedNamedImageRepresentation, + representation.shouldMakeRepresentation(inputs: inputs) { + let context = Attribute( + MakeRepresentableContext( + image: view.value, + environment: inputs.environment + ) + ) + representation.makeRepresentation( + inputs: inputs, + context: context, + outputs: &outputs + ) + } + return outputs + } + return style._makeImageView(view: view, inputs: newInputs) + } + + nonisolated private static func makeImageViewChild

( + _ type: P.Type, + image: Attribute, + options: ImageResolutionContext.Options, + inputs: _ViewInputs, + outputs: inout _ViewOutputs + ) where P: ImageAccessibilityProvider { + let child = Attribute( + ImageViewChild

( + view: image, + environment: inputs.environment, + transaction: inputs.transaction, + position: inputs.position, + size: inputs.size, + transform: inputs.transform, + options: options, + parentID: inputs.scrapeableParentID, + symbolAnimator: nil, + symbolEffects: .init() + ) + ) + child.flags = [ + .transactional, + inputs.isScrapeable ? .scrapeable : [] + ] + outputs = P.Body.makeDebuggableView(view: .init(child), inputs: inputs) + } + + private struct MakeRepresentableContext: Rule, AsyncAttribute { + @Attribute var image: Image + @Attribute var environment: EnvironmentValues + + var value: PlatformNamedImageRepresentableContext { + PlatformNamedImageRepresentableContext( + image: image, + environment: environment + ) + } + } + + private struct ImageViewChild

: StatefulRule, AsyncAttribute, ScrapeableAttribute where P: ImageAccessibilityProvider { + @Attribute var view: Image + @Attribute var environment: EnvironmentValues + @Attribute var transaction: Transaction + @Attribute var position: CGPoint + @Attribute var size: ViewSize + @Attribute var transform: ViewTransform + let options: ImageResolutionContext.Options + let parentID: ScrapeableID + let tracker: PropertyList.Tracker + var symbolAnimator: ORBSymbolAnimator? + var symbolEffects: _SymbolEffect.Phase + + init( + view: Attribute, + environment: Attribute, + transaction: Attribute, + position: Attribute, + size: Attribute, + transform: Attribute, + options: ImageResolutionContext.Options, + parentID: ScrapeableID, + symbolAnimator: ORBSymbolAnimator?, + symbolEffects: _SymbolEffect.Phase + ) { + self._view = view + self._environment = environment + self._transaction = transaction + self._position = position + self._size = size + self._transform = transform + self.options = options + self.parentID = parentID + self.tracker = .init() + self.symbolAnimator = symbolAnimator + self.symbolEffects = symbolEffects + } + + typealias Value = P.Body + + mutating func updateValue() { + let (view, viewChanged) = $view.changedValue() + let changed: Bool + if viewChanged { + changed = true + } else { + let (environment, environmentChanged) = $environment.changedValue() + if environmentChanged, tracker.hasDifferentUsedValues(environment.plist) { + changed = true + } else { + changed = !hasValue + } + } + guard changed else { return } + tracker.reset() + tracker.initializeValues(from: environment.plist) + let newEnvironment = EnvironmentValues(environment.plist, tracker: tracker) + var resolutionContext = ImageResolutionContext( + environment: newEnvironment, + textStyle: nil, + transaction: .init($transaction) + ) + resolutionContext.symbolAnimator = symbolAnimator + resolutionContext.options.formUnion(options) + var resolved = view.resolve(in: resolutionContext) + symbolAnimator = resolved.image.updateSymbolEffects( + &symbolEffects, + environment: newEnvironment, + transaction: $transaction, + animationsDisabled: options.contains(.animationsDisabled) + ) + + value = P.makeView(image: view, resolved: resolved) + } + + static func scrapeContent(from ident: AnyAttribute) -> ScrapeableContent.Item? { + let child = ident.info.body.assumingMemoryBound(to: ImageViewChild.self)[] + return ScrapeableContent.Item( + .image(child.view, child.environment), + ids: .none, + child.parentID, + position: child.$position, + size: child.$size, + transform: child.$transform + ) + } } } diff --git a/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift b/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift index 6c200fe46..156e922da 100644 --- a/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift @@ -141,50 +141,240 @@ extension Image { } } -// MARK: - Image.Resolved + View +// MARK: - Image.Resolved + View [_makeView WIP] extension Image.Resolved: UnaryView, PrimitiveView, ShapeStyledLeafView, LeafViewLayout { - package struct UpdateData {} + package struct UpdateData { + @Attribute var time: Time + @Attribute var position: ViewOrigin + @Attribute var size: ViewSize + @Attribute var pixelLength: CGFloat + } - package mutating func mustUpdate(data: Image.Resolved.UpdateData, position: Attribute) -> Bool { - _openSwiftUIUnimplementedFailure() + package mutating func mustUpdate( + data: Image.Resolved.UpdateData, + position: Attribute + ) -> Bool { + guard case let .vectorGlyph(resolvedVectorGlyph) = image.contents else { + return false + } + // TODO: ResolvedVectorGlyph + _openSwiftUIUnimplementedWarning() + return false } package func frame(in size: CGSize) -> CGRect { - _openSwiftUIUnimplementedFailure() + guard image.resizingInfo == nil else { + return CGRect(origin: .zero, size: size) + } + return CGRect( + origin: layoutMetrics?.alignmentOrigin ?? .zero, + size: self.size + ) } package func shape(in size: CGSize) -> Image.Resolved.FramedShape { - _openSwiftUIUnimplementedFailure() + (.image(image), frame(in: size)) } package static var hasBackground: Bool { - _openSwiftUIUnimplementedFailure() + true } package func backgroundShape(in size: CGSize) -> Image.Resolved.FramedShape { - _openSwiftUIUnimplementedFailure() + guard let backgroundShape, let layoutMetrics else { + return (.empty, .zero) + } + let backgroundSize = layoutMetrics.backgroundSize + let contentSize = layoutMetrics.contentSize + let size = CGSize( + width: backgroundSize.width * (size.width / contentSize.width), + height: backgroundSize.height * (size.height / contentSize.height) + ) + let rect = CGRect(origin: .zero, size: size) + let path = backgroundShape.path(in: rect, cornerRadius: backgroundCornerRadius) + return (.path(path, .init()), rect) } - package func isClear(styles: _ShapeStyle_Pack) -> Bool { - _openSwiftUIUnimplementedFailure() + package func isClear(styles: ShapeStyle.Pack) -> Bool { + switch image.contents { + case let .vectorGlyph(resolvedVectorGlyph): + return resolvedVectorGlyph.isClear(styles: styles) && styles.isClear(name: .background) + default: + return image.isTemplate && styles.isClear(name: .foreground) && styles.isClear(name: .background) + } } package func sizeThatFits(in proposedSize: _ProposedSize) -> CGSize { - _openSwiftUIUnimplementedFailure() + guard let resizingInfo = image.resizingInfo else { + return contentSize + } + let capInsets = resizingInfo.capInsets + let width = proposedSize.width.map { max($0, capInsets.horizontal) } + let height = proposedSize.height.map { max($0, capInsets.vertical) } + return CGSize(width: width ?? size.width, height: height ?? size.height) } - nonisolated package static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { - _openSwiftUIUnimplementedFailure() + nonisolated package static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + var newInputs = inputs + let imageLayoutAsText: Bool + if inputs.requestsLayoutComputer, Semantics.ImagesLayoutAsText.isEnabled { + imageLayoutAsText = true + newInputs.requestsLayoutComputer = false + } else { + imageLayoutAsText = false + } + var outputs: _ViewOutputs + if inputs.preferences.requiresDisplayList { + let pixelLength = inputs.pixelLength + if inputs.archivedView.isArchived { + // TODO: ContentTransitionEffect + _openSwiftUIUnimplementedFailure() + } else { + let group = _ShapeStyle_InterpolatorGroup() + newInputs.containerPosition = inputs.animatedPosition() + let shapeStyles = inputs.resolvedShapeStyles( + role: .stroke, + mode: view.value.styleResolverMode + ) + let data = UpdateData( + time: inputs.time, + position: inputs.position, + size: inputs.size, + pixelLength: pixelLength + ) + outputs = makeLeafView( + view: view, + inputs: newInputs, + styles: shapeStyles, + interpolatorGroup: group, + data: data + ) + // TODO: InterpolatableContent for Image.Resolved +// outputs.applyInterpolatorGroup( +// group, +// content:view.value, +// inputs: inputs, +// animatesSize: true, +// defersRender: false +// ) + } + } else { + outputs = .init() + } + if imageLayoutAsText { + outputs.layoutComputer = Attribute( + ResolvedImageLayoutComputer(image: view.value) + ) + } else { + makeLeafLayout(&outputs, view: view, inputs: newInputs) + } + if let representation = inputs.requestedImageRepresentation, + representation.shouldMakeRepresentation(inputs: inputs) { + let context = Attribute( + MakeRepresentableContext( + image: view.value, + tintColor: inputs.tintColor, + foregroundStlye: inputs.foregroundStyle + ) + ) + representation.makeRepresentation( + inputs: inputs, + context: context, + outputs: &outputs + ) + } + return outputs + } + + private struct MakeRepresentableContext: Rule, AsyncAttribute { + @Attribute var image: Image.Resolved + @Attribute var tintColor: Color? + @Attribute var foregroundStlye: AnyShapeStyle? + + var value: PlatformImageRepresentableContext { + PlatformImageRepresentableContext( + image: image, + tintColor: tintColor, + foregroundStyle: foregroundStlye + ) + } } +} + +// MARK: - ResolvedImageLayoutComputer + +private struct ResolvedImageLayoutComputer: StatefulRule, AsyncAttribute { + @Attribute var image: Image.Resolved - @available(OpenSwiftUI_v1_0, *) - package typealias Body = Never + typealias Value = LayoutComputer - @available(OpenSwiftUI_v1_0, *) - package typealias ShapeUpdateData = Image.Resolved.UpdateData + mutating func updateValue() { + let engine = ResolvedImageLayoutEngine(image: image) + update(to: engine) + } +} + +// MARK: - ResolvedImageLayoutEngine + +private struct ResolvedImageLayoutEngine: LayoutEngine { + var image: Image.Resolved + + func spacing() -> Spacing { + guard image.image.resizingInfo == nil, + let layoutMetrics = image.layoutMetrics, + image.backgroundShape == nil else { + return .init() + } + let baselineOffset = layoutMetrics.baselineOffset + let alignmentOriginY = layoutMetrics.alignmentOrigin.y + let sum = baselineOffset + alignmentOriginY + return Spacing(minima: [ + .init(category: .textToText, edge: .top): .distance(0), + .init(category: .textToText, edge: .bottom): .distance(0), + .init(category: .edgeAboveText, edge: .top): .distance(baselineOffset), + .init(category: .edgeBelowText, edge: .bottom): .distance(baselineOffset + 1.0), + .init(category: .textBaseline, edge: .bottom): .distance(-sum), + .init(category: .textBaseline, edge: .top): .distance(-(layoutMetrics.contentSize.height - sum)), + ]) + } + + func sizeThatFits(_ proposedSize: _ProposedSize) -> CGSize { + image.sizeThatFits(in: proposedSize) + } + + func lengthThatFits(_ proposal: _ProposedSize, in axis: Axis) -> CGFloat { + image.sizeThatFits(in: proposal)[axis] + } + + func explicitAlignment( + _ k: AlignmentKey, + at viewSize: ViewSize + ) -> CGFloat? { + guard image.image.resizingInfo == nil, + let layoutMetrics = image.layoutMetrics else { + return nil + } + let baselineOffset = layoutMetrics.baselineOffset + let alignmentOriginY = layoutMetrics.alignmentOrigin.y + let baseline = viewSize.height - baselineOffset - alignmentOriginY + if VerticalAlignment.lastTextBaseline.key == k { + return baseline + } else if VerticalAlignment.firstTextBaseline.key == k { + return baseline + } else if VerticalAlignment._firstTextLineCenter.key == k { + return baseline - layoutMetrics.capHeight / 2 + } else { + return nil + } + } } +// MARK: - Image.Resolved + InterpolatableContent [TODO] + //extension Image.Resolved: InterpolatableContent { // package static var defaultTransition: ContentTransition { // _openSwiftUIUnimplementedFailure()