diff --git a/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj b/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj index eb999bdcd..5cb32e17e 100644 --- a/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj +++ b/NativeDemo/TokamakDemo.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 207C05702610E16E00BBBE54 /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */; }; 207C05712610E16E00BBBE54 /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */; }; + 262DA7B32695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; }; + 262DA7B42695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; }; 3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; }; 3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; }; 4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4550BD5125B642B80088F4EA /* ShadowDemo.swift */; }; @@ -97,6 +99,7 @@ /* Begin PBXFileReference section */ 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = ""; }; + 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShapeStyleDemo.swift; sourceTree = ""; }; 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = ""; }; 4550BD5125B642B80088F4EA /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = ""; }; 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = ""; }; @@ -111,7 +114,7 @@ 85ED189A24AD425E0085DFA0 /* SpacerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacerDemo.swift; sourceTree = ""; }; 85ED189B24AD425E0085DFA0 /* TextDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextDemo.swift; sourceTree = ""; }; 85ED189C24AD425E0085DFA0 /* ForEachDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForEachDemo.swift; sourceTree = ""; }; - 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = ""; }; + 85ED189D24AD425E0085DFA0 /* TokamakDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = TokamakDemo.swift; sourceTree = ""; tabWidth = 2; }; 85ED189E24AD425E0085DFA0 /* Counter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = ""; }; 85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldDemo.swift; sourceTree = ""; }; 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = ""; }; @@ -191,6 +194,7 @@ 85ED189924AD425E0085DFA0 /* TokamakDemo */ = { isa = PBXGroup; children = ( + 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */, D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */, D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */, D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */, @@ -382,6 +386,7 @@ B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */, 3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */, 85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */, + 262DA7B32695D99500CABEAE /* ShapeStyleDemo.swift in Sources */, 85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */, D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */, 854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */, @@ -415,6 +420,7 @@ B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */, 3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */, 85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */, + 262DA7B42695D99500CABEAE /* ShapeStyleDemo.swift in Sources */, 85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */, D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */, 854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */, diff --git a/Sources/TokamakCore/Modifiers/StyleModifiers.swift b/Sources/TokamakCore/Modifiers/StyleModifiers.swift index b9d49758b..d2271a887 100644 --- a/Sources/TokamakCore/Modifiers/StyleModifiers.swift +++ b/Sources/TokamakCore/Modifiers/StyleModifiers.swift @@ -58,6 +58,33 @@ public extension View { ) -> some View where Background: View { modifier(_BackgroundModifier(background: background, alignment: alignment)) } + + @inlinable + func background( + alignment: Alignment = .center, + @ViewBuilder content: () -> V + ) -> some View where V: View { + background(content(), alignment: alignment) + } + + @inlinable + func background( + _ style: S, + in shape: T, + fillStyle: FillStyle = FillStyle() + ) -> some View where S: ShapeStyle, T: Shape { + background { + shape.fill(style, style: fillStyle) + } + } + + @inlinable + func background( + in shape: S, + fillStyle: FillStyle = FillStyle() + ) -> some View where S: Shape { + background(ForegroundStyle(), in: shape, fillStyle: fillStyle) + } } public struct _OverlayModifier: ViewModifier, EnvironmentReader diff --git a/Sources/TokamakCore/Shapes/ModifiedShapes.swift b/Sources/TokamakCore/Shapes/ModifiedShapes.swift index a40424959..4eff3696a 100644 --- a/Sources/TokamakCore/Shapes/ModifiedShapes.swift +++ b/Sources/TokamakCore/Shapes/ModifiedShapes.swift @@ -32,6 +32,8 @@ public struct _StrokedShape: Shape, DynamicProperty where S: Shape { .path(in: rect) .strokedPath(style) } + + public static var role: ShapeRole { .stroke } } public struct _TrimmedShape: Shape where S: Shape { diff --git a/Sources/TokamakCore/Shapes/Shape.swift b/Sources/TokamakCore/Shapes/Shape.swift index 2b3c055c3..1a30e68a8 100644 --- a/Sources/TokamakCore/Shapes/Shape.swift +++ b/Sources/TokamakCore/Shapes/Shape.swift @@ -19,9 +19,19 @@ import Foundation public protocol Shape: View { func path(in rect: CGRect) -> Path + + static var role: ShapeRole { get } } -public protocol ShapeStyle {} +public enum ShapeRole: Hashable { + case fill + case stroke + case separator +} + +public extension Shape { + static var role: ShapeRole { .fill } +} public extension ShapeStyle where Self: View, Self.Body == _ShapeView { var body: some View { @@ -34,11 +44,7 @@ public protocol InsettableShape: Shape { func inset(by amount: CGFloat) -> InsetShape } -public struct ForegroundStyle: ShapeStyle { - public init() {} -} - -public struct FillStyle: Equatable, ShapeStyle { +public struct FillStyle: Equatable { public var isEOFilled: Bool public var isAntialiased: Bool diff --git a/Sources/TokamakCore/Shapes/ShapeStyles/ForegroundStyle.swift b/Sources/TokamakCore/Shapes/ShapeStyles/ForegroundStyle.swift new file mode 100644 index 000000000..8efcfb13b --- /dev/null +++ b/Sources/TokamakCore/Shapes/ShapeStyles/ForegroundStyle.swift @@ -0,0 +1,30 @@ +// Copyright 2020-2021 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Carson Katri on 7/6/21. +// + +public struct ForegroundStyle: ShapeStyle { + public init() {} + + public func _apply(to shape: inout _ShapeStyle_Shape) { + if let foregroundStyle = shape.environment._foregroundStyle { + foregroundStyle._apply(to: &shape) + } else { + shape.result = .color(shape.environment.foregroundColor ?? .primary) + } + } + + public static func _apply(to shape: inout _ShapeStyle_ShapeType) {} +} diff --git a/Sources/TokamakCore/Shapes/ShapeStyles/ShapeStyle.swift b/Sources/TokamakCore/Shapes/ShapeStyles/ShapeStyle.swift new file mode 100644 index 000000000..b50ef276b --- /dev/null +++ b/Sources/TokamakCore/Shapes/ShapeStyles/ShapeStyle.swift @@ -0,0 +1,201 @@ +// Copyright 2020-2021 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Carson Katri on 7/6/21. +// + +import Foundation + +public protocol ShapeStyle { + func _apply(to shape: inout _ShapeStyle_Shape) + static func _apply(to type: inout _ShapeStyle_ShapeType) +} + +public struct AnyShapeStyle: ShapeStyle { + let styles: [ShapeStyle] + let environment: EnvironmentValues + + public func _apply(to shape: inout _ShapeStyle_Shape) { + shape.environment = environment + if styles.count > 1 { + let results = styles.map { style -> _ShapeStyle_Shape.Result in + var copy = shape + style._apply(to: ©) + return copy.result + } + shape + .result = + .resolved(.array(results.compactMap { $0.resolvedStyle(on: shape, in: environment) })) + } else if let first = styles.first { + first._apply(to: &shape) + } + + switch shape.operation { + case let .prepare(text, level): + var modifiers = text.modifiers + if let color = shape.result.resolvedStyle(on: shape, in: environment)?.color(at: level) { + modifiers.insert(.color(color), at: 0) + } + shape.result = .prepared(Text(storage: text.storage, modifiers: modifiers)) + case let .resolveStyle(levels): + if case let .resolved(resolved) = shape.result { + if case let .array(children) = resolved { + shape.result = .resolved(.array(.init(children[levels]))) + } + } else if let resolved = shape.result.resolvedStyle(on: shape, in: environment) { + shape.result = .resolved(resolved) + } + default: + // TODO: Handle other operations. + break + } + } + + public static func _apply(to type: inout _ShapeStyle_ShapeType) {} +} + +public struct _ShapeStyle_Shape { + public let operation: Operation + public var result: Result + public var environment: EnvironmentValues + public var bounds: CGRect? + public var role: ShapeRole + public var inRecursiveStyle: Bool + + public init( + for operation: Operation, + in environment: EnvironmentValues, + role: ShapeRole + ) { + self.operation = operation + result = .none + self.environment = environment + bounds = nil + self.role = role + inRecursiveStyle = false + } + + public enum Operation { + case prepare(Text, level: Int) + case resolveStyle(levels: Range) + case fallbackColor(level: Int) + case multiLevel + case copyForeground + case primaryStyle + case modifyBackground + } + + public enum Result { + case prepared(Text) + case resolved(_ResolvedStyle) + case style(AnyShapeStyle) + case color(Color) + case bool(Bool) + case none + + public func resolvedStyle(on shape: _ShapeStyle_Shape, + in environment: EnvironmentValues) -> _ResolvedStyle? + { + switch self { + case let .resolved(resolved): return resolved + case let .style(anyStyle): + var copy = shape + anyStyle._apply(to: ©) + return copy.result.resolvedStyle(on: shape, in: environment) + case let .color(color): + return .color(color.provider.resolve(in: environment)) + default: + return nil + } + } + } +} + +public struct _ShapeStyle_ShapeType {} + +public indirect enum _ResolvedStyle { + case color(AnyColorBox.ResolvedValue) +// case paint(AnyResolvedPaint) // I think is used for Image as a ShapeStyle (SwiftUI.ImagePaint). + // TODO: Material +// case foregroundMaterial(AnyColorBox.ResolvedValue, MaterialStyle) +// case backgroundMaterial(AnyColorBox.ResolvedValue) + case array([_ResolvedStyle]) + case opacity(Float, _ResolvedStyle) +// case multicolor(ResolvedMulticolorStyle) + + public func color(at level: Int) -> Color? { + switch self { + case let .color(resolved): + return Color(_ConcreteColorBox(resolved)) + case let .array(children): + return children[level].color(at: level) + case let .opacity(opacity, resolved): + guard let color = resolved.color(at: level) else { return nil } + return color.opacity(Double(opacity)) + } + } +} + +extension EnvironmentValues { + private struct ForegroundStyleKey: EnvironmentKey { + static let defaultValue: AnyShapeStyle? = nil + } + + public var _foregroundStyle: AnyShapeStyle? { + get { + self[ForegroundStyleKey.self] + } + set { + self[ForegroundStyleKey.self] = newValue + } + } +} + +public extension View { + @inlinable + func foregroundStyle(_ style: S) -> some View + where S: ShapeStyle + { + foregroundStyle(style, style, style) + } + + @inlinable + func foregroundStyle(_ primary: S1, _ secondary: S2) -> some View + where S1: ShapeStyle, S2: ShapeStyle + { + foregroundStyle(primary, secondary, secondary) + } + + @inlinable + func foregroundStyle(_ primary: S1, _ secondary: S2, + _ tertiary: S3) -> some View + where S1: ShapeStyle, S2: ShapeStyle, S3: ShapeStyle + { + modifier(_ForegroundStyleModifier(styles: [primary, secondary, tertiary])) + } +} + +@frozen public struct _ForegroundStyleModifier: ViewModifier, EnvironmentModifier { + public var styles: [ShapeStyle] + + @inlinable + public init(styles: [ShapeStyle]) { + self.styles = styles + } + + public typealias Body = Never + public func modifyEnvironment(_ values: inout EnvironmentValues) { + values._foregroundStyle = .init(styles: styles, environment: values) + } +} diff --git a/Sources/TokamakCore/Tokens/AnyTokenBox.swift b/Sources/TokamakCore/Tokens/AnyTokenBox.swift index 1c0cc44a2..820a9b536 100644 --- a/Sources/TokamakCore/Tokens/AnyTokenBox.swift +++ b/Sources/TokamakCore/Tokens/AnyTokenBox.swift @@ -16,7 +16,7 @@ // /// Allows "late-binding tokens" to be resolved in an environment by a `Renderer` (or `TokamakCore`) -public protocol AnyTokenBox: AnyObject, Hashable { +public protocol AnyTokenBox: AnyObject { associatedtype ResolvedValue func resolve(in environment: EnvironmentValues) -> ResolvedValue } diff --git a/Sources/TokamakCore/Tokens/Color.swift b/Sources/TokamakCore/Tokens/Color.swift index eb9227a51..f78f615b1 100644 --- a/Sources/TokamakCore/Tokens/Color.swift +++ b/Sources/TokamakCore/Tokens/Color.swift @@ -39,7 +39,7 @@ public protocol AnyColorBoxDeferredToRenderer: AnyColorBox { func deferredResolve(in environment: EnvironmentValues) -> AnyColorBox.ResolvedValue } -public class AnyColorBox: AnyTokenBox, Equatable { +public class AnyColorBox: AnyTokenBox, Hashable { public struct _RGBA: Hashable, Equatable { public let red: Double public let green: Double @@ -123,6 +123,38 @@ public class _EnvironmentDependentColorBox: AnyColorBox { } } +public class _OpacityColorBox: AnyColorBox { + public let parent: AnyColorBox + public let opacity: Double + + override public func equals(_ other: AnyColorBox) -> Bool { + guard let other = other as? _OpacityColorBox + else { return false } + return parent.equals(other.parent) && opacity == other.opacity + } + + override public func hash(into hasher: inout Hasher) { + hasher.combine(parent) + hasher.combine(opacity) + } + + init(_ parent: AnyColorBox, opacity: Double) { + self.parent = parent + self.opacity = opacity + } + + override public func resolve(in environment: EnvironmentValues) -> ResolvedValue { + let resolved = parent.resolve(in: environment) + return .init( + red: resolved.red, + green: resolved.green, + blue: resolved.blue, + opacity: opacity, + space: resolved.space + ) + } +} + public class _SystemColorBox: AnyColorBox, CustomStringConvertible { public enum SystemColor: String, Equatable, Hashable { case clear @@ -209,7 +241,7 @@ public struct Color: Hashable, Equatable { let provider: AnyColorBox - private init(_ provider: AnyColorBox) { + internal init(_ provider: AnyColorBox) { self.provider = provider } @@ -249,6 +281,12 @@ public struct Color: Hashable, Equatable { } } +public extension Color { + func opacity(_ opacity: Double) -> Self { + Self(_OpacityColorBox(provider, opacity: opacity)) + } +} + public struct _ColorProxy { let subject: Color public init(_ subject: Color) { self.subject = subject } @@ -343,7 +381,14 @@ public extension Color { } } -extension Color: ShapeStyle {} +extension Color: ShapeStyle { + public func _apply(to shape: inout _ShapeStyle_Shape) { + shape.result = .color(self) + } + + public static func _apply(to type: inout _ShapeStyle_ShapeType) {} +} + extension Color: View { @_spi(TokamakCore) public var body: some View { diff --git a/Sources/TokamakCore/Views/Text/Text.swift b/Sources/TokamakCore/Views/Text/Text.swift index 6ec6b2ae7..d47903c77 100644 --- a/Sources/TokamakCore/Views/Text/Text.swift +++ b/Sources/TokamakCore/Views/Text/Text.swift @@ -92,7 +92,22 @@ public extension Text._Storage { public struct _TextProxy { public let subject: Text - public init(_ subject: Text) { self.subject = subject } + public init(_ subject: Text) { + // Resolve the foregroundStyle. + if let foregroundStyle = subject.environment._foregroundStyle { + var shape = _ShapeStyle_Shape( + for: .prepare(subject, level: 0), + in: subject.environment, + role: .fill + ) + foregroundStyle._apply(to: &shape) + if case let .prepared(text) = shape.result { + self.subject = text + return + } + } + self.subject = subject + } public var storage: Text._Storage { subject.storage } public var rawText: String { diff --git a/Sources/TokamakDemo/ShapeStyleDemo.swift b/Sources/TokamakDemo/ShapeStyleDemo.swift new file mode 100644 index 000000000..ec7e43533 --- /dev/null +++ b/Sources/TokamakDemo/ShapeStyleDemo.swift @@ -0,0 +1,43 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import TokamakShim + +@available(macOS 12.0, iOS 15.0, *) +struct ShapeStyleDemo: View { + var body: some View { + #if compiler(>=5.5) || os(WASI) + HStack { + VStack { + Text("Red Style") + Rectangle() + .frame(width: 25, height: 25) + } + .foregroundStyle(Color.red) + VStack { + Text("Green Style") + Rectangle() + .frame(width: 25, height: 25) + } + .foregroundStyle(Color.green) + VStack { + Text("Blue Style") + Rectangle() + .frame(width: 25, height: 25) + } + .foregroundStyle(Color.blue) + } + #endif + } +} diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 76d198a3f..9ad0aa4e9 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -142,6 +142,9 @@ struct TokamakDemoView: View { NavItem("Preferences", destination: PreferenceKeyDemo()) } NavItem("Color", destination: ColorDemo()) + if #available(macOS 12.0, iOS 15.0, *) { + NavItem("Shape Styles", destination: ShapeStyleDemo()) + } if #available(OSX 11.0, iOS 14.0, *) { NavItem("AppStorage", destination: AppStorageDemo()) } else { diff --git a/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift b/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift index b3a7c4241..f71f51589 100644 --- a/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift +++ b/Sources/TokamakStaticHTML/Shapes/_ShapeView.swift @@ -21,9 +21,30 @@ protocol ShapeAttributes { func attributes(_ style: ShapeStyle) -> [HTMLAttribute: String] } +extension ShapeStyle { + func resolve( + for operation: _ShapeStyle_Shape.Operation, + in environment: EnvironmentValues, + role: ShapeRole + ) -> _ResolvedStyle? { + var shape = _ShapeStyle_Shape( + for: operation, + in: environment, + role: role + ) + _apply(to: &shape) + return shape.result + .resolvedStyle(on: shape, in: environment) + } +} + extension _StrokedShape: ShapeAttributes { func attributes(_ style: ShapeStyle) -> [HTMLAttribute: String] { - if let color = style as? Color { + if let color = style.resolve( + for: .resolveStyle(levels: 0..<1), + in: environment, + role: .stroke + )?.color(at: 0) { return ["style": "stroke: \(color.cssValue(environment)); fill: none;"] } else { return ["style": "stroke: black; fill: none;"] @@ -39,10 +60,20 @@ extension _ShapeView: _HTMLPrimitive { if let shapeAttributes = shape as? ShapeAttributes { attributes = shapeAttributes.attributes(style) - } else if let color = style as? Color { + } else if let color = style.resolve( + for: .resolveStyle(levels: 0..<1), + in: environment, + role: Content.role + )?.color(at: 0) { + attributes = ["style": "fill: \(color.cssValue(environment));"] + } else if let foregroundStyle = environment._foregroundStyle, + let color = foregroundStyle.resolve( + for: .resolveStyle(levels: 0..<1), + in: environment, + role: Content.role + )?.color(at: 0) + { attributes = ["style": "fill: \(color.cssValue(environment));"] - } else if let foregroundColor = foregroundColor { - attributes = ["style": "fill: \(foregroundColor.cssValue(environment));"] } else { return path } diff --git a/Tests/TokamakStaticHTMLTests/RenderingTests.swift b/Tests/TokamakStaticHTMLTests/RenderingTests.swift index 8fb224d10..086da17e6 100644 --- a/Tests/TokamakStaticHTMLTests/RenderingTests.swift +++ b/Tests/TokamakStaticHTMLTests/RenderingTests.swift @@ -15,7 +15,7 @@ // Created by Max Desiatov on 13/06/2021. // -// SnapshotTesting with image snapshots are only supported on iOS. +// SnapshotTesting with image snapshots are only supported on macOS. #if os(macOS) import SnapshotTesting import TokamakStaticHTML @@ -168,7 +168,6 @@ final class RenderingTests: XCTestCase { } func testContainerRelativeShape() { - #if compiler(>=5.5) || os(WASI) assertSnapshot( matching: ZStack { ContainerRelativeShape() @@ -179,9 +178,26 @@ final class RenderingTests: XCTestCase { .frame(width: 50, height: 50) }.containerShape(Circle()), as: .image(size: .init(width: 150, height: 150)), - timeout: 10 + timeout: defaultSnapshotTimeout + ) + } + + func testForegroundStyle() { + assertSnapshot( + matching: HStack(spacing: 0) { + Rectangle() + .frame(width: 50, height: 50) + .foregroundStyle(Color.red) + Rectangle() + .frame(width: 50, height: 50) + .foregroundStyle(Color.green) + Rectangle() + .frame(width: 50, height: 50) + .foregroundStyle(Color.blue) + }, + as: .image(size: .init(width: 200, height: 100)), + timeout: defaultSnapshotTimeout ) - #endif } } diff --git a/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testForegroundStyle.1.png b/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testForegroundStyle.1.png new file mode 100644 index 000000000..83f44ec82 Binary files /dev/null and b/Tests/TokamakStaticHTMLTests/__Snapshots__/RenderingTests/testForegroundStyle.1.png differ