diff --git a/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift b/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift index b4ce1d9d5..54d8c98ff 100644 --- a/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift +++ b/Example/OpenSwiftUIUITests/View/Image/NamedImageUITests.swift @@ -7,7 +7,10 @@ import Testing @testable import TestingHost @MainActor -@Suite(.snapshots(record: .never, diffTool: diffTool)) +@Suite( + .snapshots(record: .never, diffTool: diffTool), + .disabled("#817") +) struct NamedImageUITests { @Test("Test named image of logo with resizable") func decorativeLogo() { @@ -45,4 +48,60 @@ struct NamedImageUITests { } openSwiftUIAssertSnapshot(of: ContentView()) } + + @Test("Test different symbol varient") + func symbolVarient() { + struct ContentView: View { + let name = "document" + var body: some View { + VStack { + Image(systemName: name) + Image(systemName: name) + .symbolVariant(.circle) + Image(systemName: name) + .symbolVariant(.fill) + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test("Test symbol image with variable value") + func symbolImageWithVariableValue() { + struct ContentView: View { + let name: String = "speaker.wave.3" + var body: some View { + VStack { + Image(systemName: name, variableValue: 0) + Image(systemName: name, variableValue: 0.33) + Image(systemName: name, variableValue: 0.67) + Image(systemName: name, variableValue: 1) + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test( + "Test symbol image with different rendering mode", + .disabled("renderVectorGlyph is not supported yet") + ) + func symbolImageRenderingMode() { + struct ContentView: View { + let name: String = "gear" + var body: some View { + VStack(spacing: .zero) { + Image(systemName: name) + .foregroundStyle(.red) + Image(systemName: name) + .symbolRenderingMode(.multicolor) + .foregroundStyle(.red, .blue) + Image(systemName: name) + .symbolRenderingMode(.palette) + .foregroundStyle(.red, .blue) + }.symbolVariant(.circle) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } } diff --git a/Example/Shared/ContentView.swift b/Example/Shared/ContentView.swift index 14f5a72d9..f4b0b302b 100644 --- a/Example/Shared/ContentView.swift +++ b/Example/Shared/ContentView.swift @@ -1,9 +1,6 @@ // // ContentView.swift // Shared -// -// Created by Kyle on 2023/11/9. -// #if OPENSWIFTUI import OpenSwiftUI @@ -13,6 +10,6 @@ import SwiftUI struct ContentView: View { var body: some View { - NamedImageExample() + SunsetSceneExample() } } diff --git a/Example/Shared/View/Image/SunsetSceneExample.swift b/Example/Shared/View/Image/SunsetSceneExample.swift new file mode 100644 index 000000000..2efcda2be --- /dev/null +++ b/Example/Shared/View/Image/SunsetSceneExample.swift @@ -0,0 +1,438 @@ +// +// SunsetSceneExample.swift +// Shared + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +// MARK: - Main Scene + +struct SunsetSceneExample: View { + var body: some View { + ZStack { + SkyBackground() + SunView() + StarsView() + CloudsView() + MountainsView() + LakeView() + TreesView() + BirdsView() + SwiftLogoView() + BadgeView() + } + } +} + +// MARK: - Sky Background + +private struct SkyBackground: View { + var body: some View { + ZStack { + Color.indigo + .ignoresSafeArea() + + VStack(spacing: 0) { + Color.purple.opacity(0.5) + Color.orange.opacity(0.4) + Color.yellow.opacity(0.3) + } + .ignoresSafeArea() + } + } +} + +// MARK: - Sun + +private struct SunView: View { + @State private var sunOffset: CGFloat = 0 + @State private var glowing = false + + var body: some View { + VStack { + Spacer() + + ZStack { + Circle() + .fill(Color.orange.opacity(0.12)) + .frame(width: 200, height: 200) + Circle() + .fill(Color.orange.opacity(0.2)) + .frame(width: 150, height: 150) + Circle() + .fill(Color.yellow.opacity(0.5)) + .frame(width: 100, height: 100) + Circle() + .fill(Color.white.opacity(0.9)) + .frame(width: 55, height: 55) + } + .shadow(color: .orange.opacity(0.7), radius: glowing ? 50 : 20) + .offset(y: sunOffset) + .onAppear { + withAnimation(.easeInOut(duration: 3).repeatForever(autoreverses: true)) { + sunOffset = -10 + glowing = true + } + } + + Spacer() + } + } +} + +// MARK: - Stars + +private struct StarsView: View { + var body: some View { + VStack { + HStack(spacing: 60) { + StarIcon(opacity: 0.8, size: 10) + StarIcon(opacity: 0.5, size: 7) + StarIcon(opacity: 0.7, size: 9) + } + .padding(.top, 30) + + HStack(spacing: 90) { + StarIcon(opacity: 0.4, size: 5) + StarIcon(opacity: 0.6, size: 8) + } + .padding(.top, 8) + + HStack(spacing: 120) { + StarIcon(opacity: 0.3, size: 4) + StarIcon(opacity: 0.5, size: 6) + StarIcon(opacity: 0.4, size: 5) + } + .padding(.top, 6) + + Spacer() + } + } +} + +private struct StarIcon: View { + let opacity: Double + let size: CGFloat + + var body: some View { + Image(systemName: "star.fill") + .foregroundStyle(.white.opacity(opacity)) + .font(.system(size: size)) + } +} + +// MARK: - Clouds + +private struct CloudsView: View { + var body: some View { + VStack { + HStack { + CloudIcon(size: 40, opacity: 0.18) + Spacer() + CloudIcon(size: 30, opacity: 0.12) + .offset(y: 15) + } + .padding(.horizontal, 30) + .padding(.top, 50) + + HStack { + Spacer() + CloudIcon(size: 25, opacity: 0.1) + .padding(.trailing, 60) + } + .padding(.top, 5) + + Spacer() + } + } +} + +private struct CloudIcon: View { + let size: CGFloat + let opacity: Double + + var body: some View { + Image(systemName: "cloud.fill") + .font(.system(size: size)) + .foregroundStyle(.white.opacity(opacity)) + } +} + +// MARK: - Mountains + +private struct MountainsView: View { + var body: some View { + VStack { + Spacer() + ZStack(alignment: .bottom) { + BackMountainRange() + FrontMountainRange() + SnowCaps() + } + .frame(height: 220) + } + .ignoresSafeArea() + } +} + +private struct BackMountainRange: View { + var body: some View { + HStack(spacing: -20) { + MountainPeak(size: 120, color: Color.indigo.opacity(0.7)) + MountainPeak(size: 150, color: Color.indigo.opacity(0.6)) + MountainPeak(size: 130, color: Color.indigo.opacity(0.7)) + } + .offset(y: 40) + } +} + +private struct FrontMountainRange: View { + var body: some View { + HStack(spacing: -30) { + MountainPeak(size: 130, color: Color(red: 0.1, green: 0.06, blue: 0.18)) + MountainPeak(size: 160, color: Color(red: 0.12, green: 0.07, blue: 0.2)) + MountainPeak(size: 140, color: Color(red: 0.1, green: 0.06, blue: 0.18)) + } + .offset(y: 60) + } +} + +private struct MountainPeak: View { + let size: CGFloat + let color: Color + + var body: some View { + Image(systemName: "triangle.fill") + .font(.system(size: size)) + .foregroundStyle(color) + } +} + +private struct SnowCaps: View { + var body: some View { + HStack(spacing: -30) { + SnowCap(size: 22, offset: CGPoint(x: -5, y: -70), opacity: 0.4) + SnowCap(size: 26, offset: CGPoint(x: 15, y: -85), opacity: 0.35) + SnowCap(size: 20, offset: CGPoint(x: 30, y: -75), opacity: 0.3) + } + } +} + +private struct SnowCap: View { + let size: CGFloat + let offset: CGPoint + let opacity: Double + + var body: some View { + Image(systemName: "triangle.fill") + .font(.system(size: size)) + .foregroundStyle(.white.opacity(opacity)) + .offset(x: offset.x, y: offset.y) + } +} + +// MARK: - Lake + +private struct LakeView: View { + var body: some View { + VStack { + Spacer() + ZStack { + Color(red: 0.15, green: 0.08, blue: 0.3).opacity(0.8) + WaterShimmer() + } + .frame(height: 80) + } + .ignoresSafeArea() + } +} + +private struct WaterShimmer: View { + var body: some View { + VStack(spacing: 10) { + ForEach(0..<4, id: \.self) { i in + Capsule() + .fill(Color.orange.opacity(Double(4 - i) * 0.04)) + .frame(width: CGFloat(30 + i * 15), height: 1.2) + } + } + } +} + +// MARK: - Trees + +private struct TreesView: View { + var body: some View { + VStack { + Spacer() + HStack(alignment: .bottom) { + TreeGroup(sizes: [28, 38, 24]) + Spacer() + TreeGroup(sizes: [22, 30, 26]) + Spacer() + TreeGroup(sizes: [32, 42, 28]) + } + .padding(.horizontal, 20) + .padding(.bottom, 140) + } + } +} + +private struct TreeGroup: View { + let sizes: [CGFloat] + + var body: some View { + HStack(spacing: -4) { + ForEach(sizes, id: \.self) { size in + TreeIcon(size: size) + } + } + } +} + +private struct TreeIcon: View { + let size: CGFloat + + var body: some View { + Image(systemName: "tree.fill") + .font(.system(size: size)) + .foregroundStyle(.black.opacity(0.85)) + } +} + +// MARK: - Birds + +private struct BirdsView: View { + var body: some View { + VStack { + HStack(spacing: 10) { + BirdIcon(size: 11, offsetY: 0) + BirdIcon(size: 8, offsetY: -4) + BirdIcon(size: 10, offsetY: 2) + } + .foregroundStyle(.black.opacity(0.45)) + .padding(.top, 140) + .padding(.leading, 50) + + Spacer() + } + } +} + +private struct BirdIcon: View { + let size: CGFloat + let offsetY: CGFloat + + var body: some View { + Image(systemName: "bird.fill") + .font(.system(size: size)) + .offset(y: offsetY) + } +} + +// MARK: - Swift Logo (Colorful + Rotating) + +private struct SwiftLogoView: View { + var body: some View { + VStack { + ZStack { + // Glow behind logo + Circle() + .fill(Color.white.opacity(0.15)) + .frame(width: 90, height: 90) + + // Layered colored swift icons to simulate a colorful look + ZStack { + Image(systemName: "swift") + .font(.system(size: 52, weight: .bold)) + .foregroundStyle(.red.opacity(0.6)) + .offset(x: -2, y: -2) + + Image(systemName: "swift") + .font(.system(size: 52, weight: .bold)) + .foregroundStyle(.purple.opacity(0.5)) + .offset(x: 2, y: -1) + + Image(systemName: "swift") + .font(.system(size: 52, weight: .bold)) + .foregroundStyle(.blue.opacity(0.4)) + .offset(x: 1, y: 2) + + Image(systemName: "swift") + .font(.system(size: 50, weight: .bold)) + .foregroundStyle(.orange) + } + .shadow(color: .orange.opacity(0.5), radius: 8) + .shadow(color: .red.opacity(0.3), radius: 16) + + // Sparkle accents around logo + SparkleRing() + } + .padding(.top, 80) + + Spacer() + } + } +} + +private struct SparkleRing: View { + var body: some View { + ZStack { + Image(systemName: "sparkle") + .font(.system(size: 10)) + .foregroundStyle(.yellow.opacity(0.8)) + .offset(x: -50, y: -10) + + Image(systemName: "sparkle") + .font(.system(size: 7)) + .foregroundStyle(.orange.opacity(0.7)) + .offset(x: 48, y: 5) + + Image(systemName: "sparkle") + .font(.system(size: 8)) + .foregroundStyle(.pink.opacity(0.6)) + .offset(x: 10, y: -48) + + Image(systemName: "sparkle") + .font(.system(size: 9)) + .foregroundStyle(.white.opacity(0.5)) + .offset(x: -15, y: 46) + } + } +} + +// MARK: - Badge + +private struct BadgeView: View { + var body: some View { + VStack { + Spacer() + HStack(spacing: 8) { + Image(systemName: "moon.stars.fill") + .foregroundStyle(.yellow) + .font(.system(size: 16)) + Image(systemName: "sparkles") + .foregroundStyle(.orange) + .font(.system(size: 16)) + Image(systemName: "mountain.2.fill") + .foregroundStyle(.white.opacity(0.8)) + .font(.system(size: 16)) + } + .padding(.vertical, 10) + .padding(.horizontal, 24) + .background { + Capsule() + .fill(Color.black.opacity(0.3)) + } + // TODO: strokePath is not supported yet. +// .overlay { +// Capsule() +// .stroke(Color.white.opacity(0.2), lineWidth: 1) +// } + .padding(.bottom, 40) + } + } +} diff --git a/Example/Shared/View/Image/VariableImageExample.swift b/Example/Shared/View/Image/VariableImageExample.swift new file mode 100644 index 000000000..63966222d --- /dev/null +++ b/Example/Shared/View/Image/VariableImageExample.swift @@ -0,0 +1,44 @@ +// +// VariableImageExample.swift +// Shared + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif +#if OPENSWIFTUI_OPENCOMBINE +import OpenCombine +import OpenCombineFoundation +#else +import Combine +#endif +import Foundation + +struct VariableImageExample: View { + @State private var value: Double = 0.0 + @State private var goingUp = true + + let timer = Timer.publish(every: 0.05, on: .main, in: .common).autoconnect() + + var body: some View { + VStack(spacing: 40) { + HStack(spacing: 30) { + Image(systemName: "speaker.wave.3", variableValue: value) + Image(systemName: "wifi", variableValue: value) + Image(systemName: "chart.bar.fill", variableValue: value) + } + .font(.system(size: 60)) + .foregroundStyle(.blue) + .onReceive(timer) { _ in + if goingUp { + value += 0.02 + if value >= 1.0 { goingUp = false } + } else { + value -= 0.02 + if value <= 0.0 { goingUp = true } + } + } + } + } + } diff --git a/Package.resolved b/Package.resolved index cc0153f74..b37377f1e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "4692319eee6113f2d8ccdc00492a12e19ef48427441972e51e4f26e7cfb3e810", + "originHash" : "9cc57ad3a17b893e0be1065c0c70ebe99d327b76a45f90e3f6041166a242ff00", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "9b0f5282d12cc93e0e19c3ff1d26ee3d74458cf9" + "revision" : "bf9efed2c19a4dc296830000e707dbc7688a63fe" } }, { diff --git a/Package.swift b/Package.swift index 54d200d13..66cf97770 100644 --- a/Package.swift +++ b/Package.swift @@ -416,6 +416,7 @@ extension Target { func addOpenCombineSettings() { dependencies.append(.product(name: "OpenCombine", package: "OpenCombine")) + dependencies.append(.product(name: "OpenCombineFoundation", package: "OpenCombine")) var swiftSettings = swiftSettings ?? [] swiftSettings.append(.define("OPENSWIFTUI_OPENCOMBINE")) self.swiftSettings = swiftSettings diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift index b5c7947d8..bce5e62ad 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ShapeStyleRendering.swift @@ -90,9 +90,9 @@ package struct _ShapeStyle_RenderedShape { break case let .image(graphicsImage): if graphicsImage.isTemplate { - if case let .vectorGlyph(glygh) = graphicsImage.contents { + if case let .vectorGlyph(glyph) = graphicsImage.contents { renderVectorGlyph( - glygh, + glyph, size: graphicsImage.size, orientation: graphicsImage.orientation, name: name, @@ -249,7 +249,7 @@ package struct _ShapeStyle_RenderedShape { _openSwiftUIUnimplementedFailure() } - private func renderVectorGlyph( + private mutating func renderVectorGlyph( _ glyph: ResolvedVectorGlyph, size: CGSize, orientation: Image.Orientation, @@ -257,7 +257,17 @@ package struct _ShapeStyle_RenderedShape { styles: ShapeStyle.Pack, layers: inout ShapeStyle.RenderedLayers ) { - _openSwiftUIUnimplementedFailure() + // TODO: RBSymbolAnimator/RBSymbolUpdate + // We use a plain implementation for now + _openSwiftUIUnimplementedWarning() + let style = styles[name, 0] + layers.beginLayer( + id: .styled(name, 0), + style: style, + shape: &self + ) + render(style: style) + layers.endLayer(shape: &self) } private mutating func renderUnstyledImage( diff --git a/Sources/OpenSwiftUICore/View/Image/NamedImage.swift b/Sources/OpenSwiftUICore/View/Image/NamedImage.swift index eefe14162..6f851dd1d 100644 --- a/Sources/OpenSwiftUICore/View/Image/NamedImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/NamedImage.swift @@ -471,7 +471,7 @@ package enum NamedImage { case missingUUIDImage } - // MARK: - NamedImage.Cache [TBA] + // MARK: - NamedImage.Cache package struct Cache { private struct ImageCacheData { @@ -922,7 +922,6 @@ extension Image { ) } - #if OPENSWIFTUI_LINK_COREUI private func vectorInfo( in context: ImageResolutionContext, from catalog: CUICatalog, @@ -932,7 +931,7 @@ extension Image { let variants = environment.symbolVariants let result: NamedImage.VectorInfo? = location.findName(variants, base: name) { candidateName in vectorInfo( - name: name, + name: candidateName, in: context, from: catalog, at: location @@ -972,8 +971,6 @@ extension Image { return NamedImage.sharedCache[key, catalog] } - // TODO: ResolvedVectorGlyph - // [TBA] private func resolveVector( info: NamedImage.VectorInfo, value: Float?, @@ -981,60 +978,52 @@ extension Image { at location: Image.Location, catalog: CUICatalog ) -> Image.Resolved { - let environment = context.environment + #if OPENSWIFTUI_LINK_COREUI + var context = context let glyph = info.glyph - let flipsRightToLeft = info.flipsRightToLeft - var layoutMetrics = info.layoutMetrics - - // Create or reuse symbol animator - let animator: ORBSymbolAnimator = context.symbolAnimator ?? ORBSymbolAnimator() let resolvedVectorGlyph = ResolvedVectorGlyph( - glyph: info.glyph, + glyph: glyph, value: value, flipsRightToLeft: info.flipsRightToLeft, in: context, at: location, catalog: catalog ) + var layoutMetrics = info.layoutMetrics + var size = glyph.alignmentRect.size - let scale = glyph.scale - let alignmentRect = glyph.alignmentRect - let pixelWidth = scale * alignmentRect.width - let pixelHeight = scale * alignmentRect.height - - var graphicsImage = GraphicsImage( - contents: .vectorGlyph(resolvedVectorGlyph), - scale: scale, - unrotatedPixelSize: CGSize(width: pixelWidth, height: pixelHeight), - orientation: .up, - isTemplate: false - ) - - // Handle symbol background variant + let environment = context.environment let variants = environment.symbolVariants - var backgroundShape: SymbolVariants.Shape? - var backgroundCornerRadius: CGFloat? + + let backgroundShape: SymbolVariants.Shape? + let backgroundCornerRadius: CGFloat? if variants.contains(.background) { - let shape = variants.shape ?? .circle + let shape = variants.shape ?? .square backgroundShape = shape - let growsToFit = environment.symbolsGrowToFitBackground layoutMetrics.adjustForBackground( glyph: glyph, shape: shape, - size: &graphicsImage.unrotatedPixelSize, - growsToFitBackground: growsToFit + size: &size, + growsToFitBackground: environment.symbolsGrowToFitBackground ) backgroundCornerRadius = environment.symbolBackgroundCornerRadius + } else { + backgroundShape = nil + backgroundCornerRadius = nil } - // Handle symbol redaction + let scale = glyph.scale + var graphicsImage = GraphicsImage( + contents: .vectorGlyph(resolvedVectorGlyph), + scale: scale, + unrotatedPixelSize: size * scale, + orientation: .up, + isTemplate: true + ) + graphicsImage.allowedDynamicRange = context.effectiveAllowedDynamicRange(for: graphicsImage) if environment.shouldRedactSymbolImages { - let color = Color.foreground.resolve(in: environment) - graphicsImage.contents = GraphicsImage.Contents.color(color.multiplyingOpacity(by: 0.16)) + graphicsImage.redact(in: environment) } - - graphicsImage.allowedDynamicRange = context.effectiveAllowedDynamicRange(for: graphicsImage) - var resolved = Image.Resolved( image: graphicsImage, decorative: decorative, @@ -1044,8 +1033,10 @@ extension Image { ) resolved.layoutMetrics = layoutMetrics return resolved + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif } - #endif private func bitmapInfo( in environment: EnvironmentValues, diff --git a/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift b/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift index 4ec8deccf..b10d17714 100644 --- a/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift +++ b/Sources/OpenSwiftUICore/View/Image/ResolvedImage.swift @@ -39,36 +39,33 @@ extension Image { self.backgroundSize = .zero } - #if OPENSWIFTUI_LINK_COREUI package mutating func adjustForBackground( glyph: CUINamedVectorGlyph, shape: SymbolVariants.Shape, size: inout CGSize, growsToFitBackground: Bool ) { -// let options = CUIVectorGlyphGraphicVariantOptions() -// switch shape { -// case .circle: -// options.shape = 1 -// default: -// break -// } -// options.imageScaling = growsToFitBackground ? 1 : 3 -// guard let variant = glyph.graphicVariant(with: options) else { -// return -// } -// let interiorAlignment = variant.interiorAlignmentRect -// size = CGSize(width: interiorAlignment.width, height: interiorAlignment.height) -// baselineOffset = variant.baselineOffset -// capHeight = variant.capHeight -// let contentBounds = variant.contentBounds -// contentSize = contentBounds.size -// let alignmentRect = variant.alignmentRect -// alignmentOrigin = alignmentRect.origin -// backgroundSize = CGSize(width: alignmentRect.width, height: alignmentRect.height) + #if OPENSWIFTUI_LINK_COREUI + let options = CUIVectorGlyphGraphicVariantOptions() + options.shape = shape == .circle ? 1 : 0 + options.imageScaling = growsToFitBackground ? 1 : 3 + guard let variant = glyph.graphicVariant(with: options) else { + return + } + let interiorAlignment = variant.interiorAlignmentRect + size = interiorAlignment.size + baselineOffset = variant.baselineOffset + capHeight = variant.capHeight + contentSize = variant.contentBounds.size + alignmentOrigin = CGPoint(x: interiorAlignment.origin.x, y: interiorAlignment.origin.y) + backgroundSize = variant.alignmentRect.size + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif } package init(glyph: CUINamedVectorGlyph, flipsRightToLeft: Bool) { + #if OPENSWIFTUI_LINK_COREUI let baselineOffset = glyph.baselineOffset let capHeight = glyph.capHeight @@ -102,8 +99,10 @@ extension Image { contentSize: contentSize, alignmentOrigin: alignmentOrigin ) + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif } - #endif } // MARK: - Image.Resolved