From 96fbd5a47eed26b5b46bc7861cf808a93c7ec7a5 Mon Sep 17 00:00:00 2001 From: Jeff Verkoeyen Date: Sun, 4 Aug 2024 11:17:44 -0400 Subject: [PATCH] Add backgroundImage modifier. Part of https://github.com/jverkoey/slipstream/issues/51. --- .../Backgrounds-BackgroundImage.md | 15 ++++ .../TailwindCSS/Borders/Borders-Border.md | 2 + .../Views/TailwindCSS/TailwindCSS-Classes.md | 4 + .../Backgrounds/View+backgroundImage.swift | 85 +++++++++++++++++++ .../TailwindCSS/Borders/View+border.swift | 2 +- Sources/Slipstream/TailwindCSS/Size.swift | 31 +++++++ .../Sites/CatalogSiteTests.swift | 3 +- .../Backgrounds/BackgroundImageTests.swift | 26 ++++++ 8 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Backgrounds/Backgrounds-BackgroundImage.md create mode 100644 Sources/Slipstream/TailwindCSS/Backgrounds/View+backgroundImage.swift create mode 100644 Sources/Slipstream/TailwindCSS/Size.swift create mode 100644 Tests/SlipstreamTests/TailwindCSS/Backgrounds/BackgroundImageTests.swift diff --git a/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Backgrounds/Backgrounds-BackgroundImage.md b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Backgrounds/Backgrounds-BackgroundImage.md new file mode 100644 index 0000000..e3fdb5b --- /dev/null +++ b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Backgrounds/Backgrounds-BackgroundImage.md @@ -0,0 +1,15 @@ +# Background image + +Utilities for controlling the background image of a view. + +## Topics + +### Modifiers + +- ``View/backgroundImage(_:size:repeat:condition:)`` + +### Supporting types + +- ``BackgroundRepeat`` +- ``BackgroundSize`` +- ``Size`` diff --git a/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Borders/Borders-Border.md b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Borders/Borders-Border.md index ed57234..c7722b6 100644 --- a/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Borders/Borders-Border.md +++ b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/Borders/Borders-Border.md @@ -4,4 +4,6 @@ Utilities for controlling the border of a view. ## Topics +### Modifiers + - ``View/border(_:width:edges:condition:)`` diff --git a/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/TailwindCSS-Classes.md b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/TailwindCSS-Classes.md index 93b6a95..4b00fe7 100644 --- a/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/TailwindCSS-Classes.md +++ b/Sources/Slipstream/Documentation.docc/Views/TailwindCSS/TailwindCSS-Classes.md @@ -29,6 +29,10 @@ Slipstream implementations of Tailwind CSS's classes. - - +### Backgrounds + +- + ### Borders - diff --git a/Sources/Slipstream/TailwindCSS/Backgrounds/View+backgroundImage.swift b/Sources/Slipstream/TailwindCSS/Backgrounds/View+backgroundImage.swift new file mode 100644 index 0000000..a19bb02 --- /dev/null +++ b/Sources/Slipstream/TailwindCSS/Backgrounds/View+backgroundImage.swift @@ -0,0 +1,85 @@ +import Foundation + +/// Constants that control the repitition of a background image.. +/// +/// - SeeAlso: Tailwind CSS' [`background repeat`](https://tailwindcss.com/docs/background-repeat) documentation. +@available(iOS 17.0, macOS 14.0, *) +public enum BackgroundRepeat: String { + case yes = "repeat" + case no = "no-repeat" + case x = "repeat-x" + case y = "repeat-y" + case round = "repeat-round" + case space = "repeat-space" +} + +/// Constants that control the size of a background image.. +/// +/// - SeeAlso: Tailwind CSS' [`background size`](https://tailwindcss.com/docs/background-size) documentation. +@available(iOS 17.0, macOS 14.0, *) +public struct BackgroundSize { + /// Displays the background image at its default size. + public static let auto: BackgroundSize = BackgroundSize(storage: .aspectRatio(.auto)) + + /// Scales the background image until it fills the background layer. + public static let cover: BackgroundSize = BackgroundSize(storage: .aspectRatio(.cover)) + + /// Scales the background image to the outer edges without cropping or stretching. + public static let contain: BackgroundSize = BackgroundSize(storage: .aspectRatio(.contain)) + + /// Specifies an arbitrary size to use for the background image. + public static func size(width: Double, height: Double) -> BackgroundSize { + return BackgroundSize( + storage: .size(.init(width: width, height: height)) + ) + } + + fileprivate init(storage: Storage) { + self.storage = storage + } + + fileprivate enum AspectRatio: String { + case auto + case cover + case contain + } + fileprivate enum Storage { + case aspectRatio(AspectRatio) + case size(Size) + } + fileprivate let storage: Storage + + fileprivate func toTailwindSpacingClass() -> String? { + switch storage { + case .aspectRatio(let aspectRatio): + return "bg-" + aspectRatio.rawValue + case .size(let size): + guard let pixels = size.asPixels else { + return nil + } + return "bg-[length:\(pixels)]" + } + } +} + +extension View { + /// Changes the background image of the view. + /// + /// - SeeAlso: Tailwind CSS' [`background image`](https://tailwindcss.com/docs/background-image) documentation. + /// - SeeAlso: Tailwind CSS' [`background repeat`](https://tailwindcss.com/docs/background-repeat) documentation. + /// - SeeAlso: Tailwind CSS' [`background size`](https://tailwindcss.com/docs/background-size) documentation. + @available(iOS 17.0, macOS 14.0, *) + public func backgroundImage(_ url: URL?, size: BackgroundSize? = nil, repeat: BackgroundRepeat? = nil, condition: Condition? = nil) -> some View { + var classNames: [String] = [] + if let url { + classNames.append("bg-[url('\(url.path())')]") + } + if let size = size?.toTailwindSpacingClass() { + classNames.append(size) + } + if let `repeat` { + classNames.append("bg-\(`repeat`.rawValue)") + } + return modifier(TailwindClassModifier(add: Set(classNames), condition: condition)) + } +} diff --git a/Sources/Slipstream/TailwindCSS/Borders/View+border.swift b/Sources/Slipstream/TailwindCSS/Borders/View+border.swift index eaf1f9d..86b9675 100644 --- a/Sources/Slipstream/TailwindCSS/Borders/View+border.swift +++ b/Sources/Slipstream/TailwindCSS/Borders/View+border.swift @@ -28,7 +28,7 @@ extension View { } } classNames.append("border-" + color.toTailwindColorClass()) - return self.modifier(TailwindClassModifier(add: Set(classNames), condition: condition)) + return modifier(TailwindClassModifier(add: Set(classNames), condition: condition)) } private func closestTailwindBorderWidth(width: Int) -> String { diff --git a/Sources/Slipstream/TailwindCSS/Size.swift b/Sources/Slipstream/TailwindCSS/Size.swift new file mode 100644 index 0000000..e289a78 --- /dev/null +++ b/Sources/Slipstream/TailwindCSS/Size.swift @@ -0,0 +1,31 @@ +import Foundation + +/// A structure that represents the edges of a rectangle, used for specifying +/// which sides to apply modifications to, such as padding or margin. +/// +/// You'll typically work with a set of edges using the ``Edge/Set`` type. +@available(iOS 17.0, macOS 14.0, *) +public struct Size { + /// Creates a size. + /// + /// - Parameters: + /// - width: The width, in points. + /// - height: The height, in points. + public init(width: Double, height: Double) { + self.width = width + self.height = height + } + + let width: Double + let height: Double + + var asPixels: String? { + let formatter = NumberFormatter() + formatter.maximumFractionDigits = 2 + guard let width = formatter.string(from: width as NSNumber), + let height = formatter.string(from: height as NSNumber) else { + return nil + } + return "\(width)px_\(height)px" + } +} diff --git a/Tests/SlipstreamTests/Sites/CatalogSiteTests.swift b/Tests/SlipstreamTests/Sites/CatalogSiteTests.swift index c46ab98..12275f8 100644 --- a/Tests/SlipstreamTests/Sites/CatalogSiteTests.swift +++ b/Tests/SlipstreamTests/Sites/CatalogSiteTests.swift @@ -52,6 +52,7 @@ private struct CatalogSite: View { .flexGap(.x, width: 2) } .border(.black, width: 4, edges: .bottom) + .backgroundImage(URL(string: "/logo.svg"), size: .size(width: 50, height: 100), repeat: .no) .textColor(.red, darkness: 800, condition: .dark) .padding(.horizontal, 48, condition: .minBreakpoint(.large)) .animation(.easeInOut(duration: 0.3)) @@ -73,7 +74,7 @@ struct CatalogSiteTests { -
+
Hello
world! About diff --git a/Tests/SlipstreamTests/TailwindCSS/Backgrounds/BackgroundImageTests.swift b/Tests/SlipstreamTests/TailwindCSS/Backgrounds/BackgroundImageTests.swift new file mode 100644 index 0000000..4280840 --- /dev/null +++ b/Tests/SlipstreamTests/TailwindCSS/Backgrounds/BackgroundImageTests.swift @@ -0,0 +1,26 @@ +import Foundation +import Testing + +import Slipstream + +struct BackgroundImageTests { + @Test func justURL() throws { + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"))) == #"
"#) + } + + @Test func withRepeat() throws { + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .no)) == #"
"#) + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .yes)) == #"
"#) + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .x)) == #"
"#) + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .y)) == #"
"#) + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .round)) == #"
"#) + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .space)) == #"
"#) + } + + @Test func withSize() throws { + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), size: .auto)) == #"
"#) + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), size: .contain)) == #"
"#) + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), size: .cover)) == #"
"#) + try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), size: .size(width: 100, height: 50))) == #"
"#) + } +}