diff --git a/Example/OpenSwiftUIUITests/Shape/ShapeStyle/BackgroundStyleUITests.swift b/Example/OpenSwiftUIUITests/Shape/ShapeStyle/BackgroundStyleUITests.swift new file mode 100644 index 000000000..0cac7d792 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Shape/ShapeStyle/BackgroundStyleUITests.swift @@ -0,0 +1,113 @@ +// +// BackgroundStyleUITests.swift +// OpenSwiftUIUITests + +import Testing +import SnapshotTesting + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct BackgroundStyleUITests { + @Test + func backgroundDefault() { + struct ContentView: View { + var body: some View { + Rectangle() + .fill(.background) + .frame(width: 100, height: 100) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test(.disabled { + #if os(macOS) + // FIXME: Update RenderShape DL + true + #else + false + #endif + }) + func backgroundInGroup() { + struct ContentView: View { + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(.background) + .frame(width: 100, height: 50) + Rectangle() + .fill(.background) + .frame(width: 100, height: 50) + ._addingBackgroundGroup() + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func backgroundInLayer() { + struct ContentView: View { + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(.background) + .frame(width: 100, height: 50) + Rectangle() + .fill(.background) + .frame(width: 100, height: 50) + ._addingBackgroundLayer() + } + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func backgroundCustomStyle() { + struct ContentView: View { + var body: some View { + Rectangle() + .fill(.background) + .frame(width: 100, height: 100) + .backgroundStyle(.blue) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func backgroundNestedCustomStyle() { + struct ContentView: View { + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(.background) + .frame(width: 100, height: 50) + Rectangle() + .fill(.background) + .frame(width: 100, height: 50) + .backgroundStyle(.red) + } + .backgroundStyle(.blue) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func backgroundWithContent() { + struct ContentView: View { + var body: some View { + ZStack { + Rectangle() + .fill(.background) + Capsule() + .foregroundStyle(.primary) + } + .frame(width: 100, height: 100) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } +} diff --git a/Example/OpenSwiftUIUITests/Shape/ShapeStyle/ForegroundStyleUITests.swift b/Example/OpenSwiftUIUITests/Shape/ShapeStyle/ForegroundStyleUITests.swift new file mode 100644 index 000000000..c736dc776 --- /dev/null +++ b/Example/OpenSwiftUIUITests/Shape/ShapeStyle/ForegroundStyleUITests.swift @@ -0,0 +1,120 @@ +// +// ForegroundStyleUITests.swift +// OpenSwiftUIUITests + +import Testing +import SnapshotTesting + +@MainActor +@Suite(.snapshots(record: .never, diffTool: diffTool)) +struct ForegroundStyleUITests { + @Test + func foregroundDefault() { + struct ContentView: View { + var body: some View { + Rectangle() + .fill(.foreground) + .frame(width: 100, height: 100) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func foregroundSingleStyle() { + struct ContentView: View { + var body: some View { + Rectangle() + .frame(width: 100, height: 100) + .foregroundStyle(.blue) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func foregroundPairStyle() { + struct ContentView: View { + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(.primary) + .frame(width: 100, height: 50) + Rectangle() + .fill(.secondary) + .frame(width: 100, height: 50) + } + .foregroundStyle(.red, .blue) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func foregroundTripleStyle() { + struct ContentView: View { + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(.primary) + .frame(width: 100, height: 34) + Rectangle() + .fill(.secondary) + .frame(width: 100, height: 33) + Rectangle() + .fill(.tertiary) + .frame(width: 100, height: 33) + } + .foregroundStyle(.red, .green, .blue) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func foregroundNestedStyle() { + struct ContentView: View { + var body: some View { + VStack(spacing: 0) { + Rectangle() + .frame(width: 100, height: 50) + Rectangle() + .frame(width: 100, height: 50) + .foregroundStyle(.red) + } + .foregroundStyle(.blue) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test(.disabled("Text is not supported yet")) + func foregroundWithText() { + struct ContentView: View { + var body: some View { + Text("Hello") + .font(.largeTitle) + .foregroundStyle(.blue) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } + + @Test + func foregroundHierarchical() { + struct ContentView: View { + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(.primary) + .frame(width: 100, height: 50) + Rectangle() + .fill(.secondary) + .frame(width: 100, height: 50) + } + .foregroundStyle(.blue) + } + } + openSwiftUIAssertSnapshot(of: ContentView()) + } +} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/BackgroundStyle.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/BackgroundStyle.swift index 03b5bd66a..8b789ac26 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/BackgroundStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/BackgroundStyle.swift @@ -2,11 +2,75 @@ // BackgroundStyle.swift // OpenSwiftUICore // -// Audited for 6.0.87 -// Status: WIP +// Audited for 6.5.4 +// Status: Complete // ID: C7D4771CFE453D905E7BCD5A907D32EB (SwiftUICore) +import OpenAttributeGraphShims + +// MARK: - Color + background + +@available(OpenSwiftUI_v1_0, *) +extension Color { + private struct BackgroundColorProvider: ColorProvider { + func resolve(in environment: EnvironmentValues) -> Color.Resolved { + Color.systemBackgroundColor( + info: environment.backgroundInfo, + context: environment.backgroundContext, + scheme: environment.colorScheme + ) + } + } + + package static var _background: Color { + Color(provider: BackgroundColorProvider()) + } + + package static func systemBackgroundColor( + info: BackgroundInfo, + context: BackgroundContext, + scheme: ColorScheme + ) -> Color.Resolved { + #if os(iOS) || os(visionOS) + switch scheme { + case .light: + let combined = info.groupCount + context.rawValue + if combined & 1 == 0 { + return .white + } else { + return Color.Resolved(red: 242 / 255, green: 242 / 255, blue: 247 / 255) + } + case .dark: + let level = info.layer + info.groupCount + switch level { + case 0: + return .black + case 1: + return Color.Resolved(red: 28 / 255, green: 28 / 255, blue: 30 / 255) + case 2: + return Color.Resolved(red: 44 / 255, green: 44 / 255, blue: 46 / 255) + default: + return Color.Resolved(red: 58 / 255, green: 58 / 255, blue: 60 / 255) + } + } + #elseif os(macOS) + switch scheme { + case .light: + return .white + case .dark: + return Color.Resolved(red: 30 / 255, green: 30 / 255, blue: 30 / 255) + } + #else + return Color.Resolved.gray_75 + #endif + } +} + +// MARK: - ShapeStyle + BackgroundStyle + +@available(OpenSwiftUI_v2_0, *) extension ShapeStyle where Self == BackgroundStyle { + /// The background style in the current context. /// /// Access this value to get the style OpenSwiftUI uses for the background @@ -21,35 +85,249 @@ extension ShapeStyle where Self == BackgroundStyle { } } +// MARK: - BackgroundStyle + +/// The background style in the current context. +/// +/// You can also use ``ShapeStyle/background`` to construct this style. +@available(OpenSwiftUI_v2_0, *) @frozen public struct BackgroundStyle: ShapeStyle { static let shared = AnyShapeStyle(BackgroundStyle()) - + + /// Creates a background style instance. @inlinable public init() {} - - // TODO + + nonisolated public static func _makeView(view: _GraphValue<_ShapeView>, inputs: _ViewInputs) -> _ViewOutputs where S: Shape { + legacyMakeShapeView(view: view, inputs: inputs) + } + + private func base(level: Int, env: EnvironmentValues) -> some ShapeStyle { + var info = env.backgroundInfo + info.groupCount += level + let resolved = Color.systemBackgroundColor( + info: info, + context: env.backgroundContext, + scheme: env.colorScheme + ) + return resolved + } + + @available(OpenSwiftUI_v3_0, *) + public func _apply(to shape: inout _ShapeStyle_Shape) { + if !shape.activeRecursiveStyles.contains(.background) { + if let style = shape.environment.currentBackgroundStyle { + shape.activeRecursiveStyles.formUnion(.background) + style._apply(to: &shape) + if shape.activeRecursiveStyles.contains(.background) { + shape.activeRecursiveStyles.subtract(.background) + } + return + } + } + switch shape.operation { + case let .resolveStyle(name, _) where name == .background: + HierarchicalShapeStyle.quaternary._apply(to: &shape) + case let .resolveStyle(name, levels): + guard !levels.isEmpty else { return } + var pack = shape.stylePack + for level in levels { + var innerShape = shape + base(level: level, env: shape.environment)._apply(to: &innerShape) + let style = innerShape.stylePack[name, 0] + shape.stylePack[name, level] = style + } + shape.result = .pack(shape.stylePack) + case let .prepareText(level): + base(level: level, env: shape.environment)._apply(to: &shape) + case let .fallbackColor(level): + base(level: level, env: shape.environment)._apply(to: &shape) + case .copyStyle, .primaryStyle: + shape.result = .style(AnyShapeStyle(BackgroundStyle())) + case let .modifyBackground(level): + shape.environment.backgroundInfo.groupCount += level + case .multiLevel: + shape.result = .bool(false) + } + } + + public static func _apply(to type: inout _ShapeStyle_ShapeType) { + type.result = .bool(true) + } } +// MARK: - View + _addingBackgroundGroup / _addingBackgroundLayer + +@available(OpenSwiftUI_v1_0, *) +extension View { + + /// Add a background group, affecting the default background color. + @MainActor + @preconcurrency + public func _addingBackgroundGroup() -> some View { + transformEnvironment(\.backgroundInfo) { $0.groupCount += 1 } + } + + /// Add a background layer, affecting the default background color. + @MainActor + @preconcurrency + public func _addingBackgroundLayer() -> some View { + transformEnvironment(\.backgroundInfo) { $0.layer += 1 } + } + + /// Sets the specified style to render backgrounds within the view. + /// + /// The following example uses this modifier to set the + /// ``EnvironmentValues/backgroundStyle`` environment value to a + /// ``ShapeStyle/blue`` color that includes a subtle ``Color/gradient``. + /// OpenSwiftUI fills the ``Circle`` shape that acts as a background element + /// with this style: + /// + /// Image(systemName: "swift") + /// .padding() + /// .background(in: Circle()) + /// .backgroundStyle(.blue.gradient) + /// + /// To restore the default background style, set the + /// ``EnvironmentValues/backgroundStyle`` environment value to + /// `nil` using the ``View/environment(_:_:)`` modifer: + /// + /// .environment(\.backgroundStyle, nil) + /// + @available(OpenSwiftUI_v4_0, *) + @inlinable + nonisolated public func backgroundStyle(_ style: S) -> some View where S: ShapeStyle { + return modifier(_EnvironmentBackgroundStyleModifier(style: style)) + } +} + + // MARK: - BackgroundStyleKey private struct BackgroundStyleKey: EnvironmentKey { static let defaultValue: AnyShapeStyle? = nil } -// MARK: - EnvironmentValues + ForegroundStyle +// MARK: - BackgroundContextKey + +private struct BackgroundContextKey: EnvironmentKey { + static let defaultValue: BackgroundContext = .normal +} + +// MARK: - BackgroundInfoKey + +private struct BackgroundInfoKey: EnvironmentKey { + static let defaultValue = BackgroundInfo(layer: 0, groupCount: 0) +} + +// MARK: - EnvironmentValues + BackgroundStyle extension EnvironmentValues { - package var backgroundStyle: AnyShapeStyle? { + + /// An optional style that overrides the default system background + /// style when set. + @available(OpenSwiftUI_v4_0, *) + public var backgroundStyle: AnyShapeStyle? { get { self[BackgroundStyleKey.self] } set { self[BackgroundStyleKey.self] = newValue } } - + package var currentBackgroundStyle: AnyShapeStyle? { backgroundStyle } - + package var effectiveBackgroundStyle: AnyShapeStyle { currentBackgroundStyle ?? BackgroundStyle.shared } + + package var backgroundContext: BackgroundContext { + get { self[BackgroundContextKey.self] } + set { self[BackgroundContextKey.self] = newValue } + } + + package var backgroundInfo: BackgroundInfo { + get { self[BackgroundInfoKey.self] } + set { self[BackgroundInfoKey.self] = newValue } + } +} + +// MARK: - BackgroundContext + +package enum BackgroundContext: Int, CaseIterable { + case normal + case grouped +} + +// MARK: - BackgroundInfo + +package struct BackgroundInfo: Equatable { + package var layer: Int + + package var groupCount: Int + + package init(layer: Int, groupCount: Int) { + self.layer = layer + self.groupCount = groupCount + } } + +// MARK: - _EnvironmentBackgroundStyleModifier + +@available(OpenSwiftUI_v4_0, *) +@frozen +@MainActor +@preconcurrency +public struct _EnvironmentBackgroundStyleModifier: ViewInputsModifier, PrimitiveViewModifier where S: ShapeStyle { + @usableFromInline + var style: S + + @inlinable + init(style: S) { + self.style = style + } + + nonisolated public static func _makeViewInputs( + modifier: _GraphValue, + inputs: inout _ViewInputs + ) { + _makeInputs(modifier: modifier, inputs: &inputs.base) + } + + nonisolated private static func _makeInputs( + modifier: _GraphValue, + inputs: inout _GraphInputs + ) { + inputs.environment = Attribute( + ChildEnvironment( + modifier: modifier.value, + environment: inputs.environment + ) + ) + } + + nonisolated public static func _makeViewList( + modifier: _GraphValue, + inputs: _ViewListInputs, + body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs + ) -> _ViewListOutputs { + var inputs = inputs + _makeInputs(modifier: modifier, inputs: &inputs.base) + return body(_Graph(), inputs) + } + + private struct ChildEnvironment: Rule, AsyncAttribute { + @Attribute var modifier: _EnvironmentBackgroundStyleModifier + @Attribute var environment: EnvironmentValues + + var value: EnvironmentValues { + var environment = environment + let style = modifier.style.copyStyle(name: .background, in: environment) + environment.backgroundStyle = style + return environment + } + } +} + +@available(*, unavailable) +extension _EnvironmentBackgroundStyleModifier: Sendable {} diff --git a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ForegroundStyle.swift b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ForegroundStyle.swift index d9226edcc..694eeb384 100644 --- a/Sources/OpenSwiftUICore/Shape/ShapeStyle/ForegroundStyle.swift +++ b/Sources/OpenSwiftUICore/Shape/ShapeStyle/ForegroundStyle.swift @@ -2,12 +2,179 @@ // ForegroundStyle.swift // OpenSwiftUICore // -// Audited for 6.0.87 +// Audited for 6.5.4 // Status: Complete // ID: BEFE9363F68E039B4AB6422B8AA4535A (SwiftUICore) package import OpenAttributeGraphShims +// MARK: - View + foregroundStyle + +@available(OpenSwiftUI_v3_0, *) +extension View { + /// Sets a view's foreground elements to use a given style. + /// + /// Use this method to style + /// foreground content like text, shapes, and template images + /// (including symbols): + /// + /// HStack { + /// Image(systemName: "triangle.fill") + /// Text("Hello, world!") + /// RoundedRectangle(cornerRadius: 5) + /// .frame(width: 40, height: 20) + /// } + /// .foregroundStyle(.teal) + /// + /// The example above creates a row of ``ShapeStyle/teal`` foreground + /// elements: + /// + /// ![A screenshot of a teal triangle, string, and rounded + /// rectangle.](View-foregroundStyle-1) + /// + /// You can use any style that conforms to the ``ShapeStyle`` protocol, + /// like the ``ShapeStyle/teal`` color in the example above, or the + /// ``ShapeStyle/linearGradient(colors:startPoint:endPoint:)`` gradient + /// shown below: + /// + /// Text("Gradient Text") + /// .font(.largeTitle) + /// .foregroundStyle( + /// .linearGradient( + /// colors: [.yellow, .blue], + /// startPoint: .top, + /// endPoint: .bottom + /// ) + /// ) + /// + /// ![A screenshot of the words Gradient Text, with letters that + /// appear yellow at the top, and transition to blue + /// toward the bottom.](View-foregroundStyle-2) + /// + /// > Tip: If you want to fill a single ``Shape`` instance with a style, + /// use the ``Shape/fill(style:)`` shape modifier instead because it's more + /// efficient. + /// + /// OpenSwiftUI creates a context-dependent render for a given style. + /// For example, a ``Color`` that you load from an asset catalog + /// can have different light and dark appearances, while some styles + /// also vary by platform. + /// + /// Hierarchical foreground styles like ``ShapeStyle/secondary`` + /// don't impose a style of their own, but instead modify other styles. + /// In particular, they modify the primary + /// level of the current foreground style to the degree given by + /// the hierarchical style's name. + /// To find the current foreground style to modify, OpenSwiftUI looks for + /// the innermost containing style that you apply with the + /// `foregroundStyle(_:)` or the ``View/foregroundColor(_:)`` modifier. + /// If you haven't specified a style, OpenSwiftUI uses the default foreground + /// style, as in the following example: + /// + /// VStack(alignment: .leading) { + /// Label("Primary", systemImage: "1.square.fill") + /// Label("Secondary", systemImage: "2.square.fill") + /// .foregroundStyle(.secondary) + /// } + /// + /// ![A screenshot of two labels with the text primary and secondary. + /// The first appears in a brighter shade than the + /// second, both in a grayscale color.](View-foregroundStyle-3) + /// + /// If you add a foreground style on the enclosing + /// ``VStack``, the hierarchical styling responds accordingly: + /// + /// VStack(alignment: .leading) { + /// Label("Primary", systemImage: "1.square.fill") + /// Label("Secondary", systemImage: "2.square.fill") + /// .foregroundStyle(.secondary) + /// } + /// .foregroundStyle(.blue) + /// + /// ![A screenshot of two labels with the text primary and secondary. + /// The first appears in a brighter shade than the + /// second, both tinted blue.](View-foregroundStyle-4) + /// + /// When you apply a custom style to a view, the view disables the vibrancy + /// effect for foreground elements in that view, or in any of its child + /// views, that it would otherwise gain from adding a background material + /// --- for example, using the ``View/background(_:ignoresSafeAreaEdges:)`` + /// modifier. However, hierarchical styles applied to the default foreground + /// don't disable vibrancy. + /// + /// - Parameter style: The color or pattern to use when filling in the + /// foreground elements. To indicate a specific value, use ``Color`` or + /// ``ShapeStyle/image(_:sourceRect:scale:)``, or one of the gradient + /// types, like + /// ``ShapeStyle/linearGradient(colors:startPoint:endPoint:)``. To set a + /// style that’s relative to the containing view's style, use one of the + /// semantic styles, like ``ShapeStyle/primary``. + /// + /// - Returns: A view that uses the given foreground style. + @inlinable + nonisolated public func foregroundStyle(_ style: S) -> some View where S: ShapeStyle { + modifier(_ForegroundStyleModifier(style: style)) + } + + /// Sets the primary and secondary levels of the foreground + /// style in the child view. + /// + /// OpenSwiftUI uses these styles when rendering child views + /// that don't have an explicit rendering style, like images, + /// text, shapes, and so on. + /// + /// Symbol images within the view hierarchy use the + /// ``SymbolRenderingMode/palette`` rendering mode when you apply this + /// modifier, if you don't explicitly specify another mode. + /// + /// - Parameters: + /// - primary: The primary color or pattern to use when filling in + /// the foreground elements. To indicate a specific value, use ``Color`` + /// or ``ShapeStyle/image(_:sourceRect:scale:)``, or one of the gradient + /// types, like + /// ``ShapeStyle/linearGradient(colors:startPoint:endPoint:)``. To set a + /// style that’s relative to the containing view's style, use one of the + /// semantic styles, like ``ShapeStyle/primary``. + /// - secondary: The secondary color or pattern to use when + /// filling in the foreground elements. + /// + /// - Returns: A view that uses the given foreground styles. + @inlinable + nonisolated public func foregroundStyle(_ primary: S1, _ secondary: S2) -> some View where S1: ShapeStyle, S2: ShapeStyle { + modifier(_ForegroundStyleModifier2(primary: primary, secondary: secondary)) + } + + /// Sets the primary, secondary, and tertiary levels of + /// the foreground style. + /// + /// OpenSwiftUI uses these styles when rendering child views + /// that don't have an explicit rendering style, like images, + /// text, shapes, and so on. + /// + /// Symbol images within the view hierarchy use the + /// ``SymbolRenderingMode/palette`` rendering mode when you apply this + /// modifier, if you don't explicitly specify another mode. + /// + /// - Parameters: + /// - primary: The primary color or pattern to use when filling in + /// the foreground elements. To indicate a specific value, use ``Color`` + /// or ``ShapeStyle/image(_:sourceRect:scale:)``, or one of the gradient + /// types, like + /// ``ShapeStyle/linearGradient(colors:startPoint:endPoint:)``. To set a + /// style that’s relative to the containing view's style, use one of the + /// semantic styles, like ``ShapeStyle/primary``. + /// - secondary: The secondary color or pattern to use when + /// filling in the foreground elements. + /// - tertiary: The tertiary color or pattern to use when + /// filling in the foreground elements. + /// + /// - Returns: A view that uses the given foreground styles. + @inlinable + nonisolated public func foregroundStyle(_ primary: S1, _ secondary: S2, _ tertiary: S3) -> some View where S1: ShapeStyle, S2: ShapeStyle, S3: ShapeStyle { + modifier(_ForegroundStyleModifier3(primary: primary, secondary: secondary, tertiary: tertiary)) + } +} + // MARK: - ForegroundStyleKey private struct ForegroundStyleKey: EnvironmentKey { @@ -27,18 +194,18 @@ extension EnvironmentValues { get { self[ForegroundStyleKey.self] } set { self[ForegroundStyleKey.self] = newValue } } - + package var defaultForegroundStyle: AnyShapeStyle? { get { self[DefaultForegroundStyleKey.self] } set { self[DefaultForegroundStyleKey.self] = newValue } } - + package var currentForegroundStyle: AnyShapeStyle? { foregroundStyle ?? defaultForegroundStyle } - + package var _effectiveForegroundStyle: AnyShapeStyle { - currentForegroundStyle ?? .init(.foreground) + currentForegroundStyle ?? HierarchicalShapeStyle.sharedPrimary } } @@ -52,8 +219,180 @@ extension _ViewInputs { } } +// MARK: - _DefaultForegroundStyleModifier + +struct _DefaultForegroundStyleModifier