Skip to content

Commit

Permalink
Add backgroundImage modifier.
Browse files Browse the repository at this point in the history
Part of #51.
  • Loading branch information
jverkoey committed Aug 4, 2024
1 parent 27e8a55 commit 96fbd5a
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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``
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ Utilities for controlling the border of a view.

## Topics

### Modifiers

- ``View/border(_:width:edges:condition:)``
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Slipstream implementations of Tailwind CSS's classes.
- <doc:Typography-TextAlignment>
- <doc:Typography-TextColor>

### Backgrounds

- <doc:Backgrounds-BackgroundImage>

### Borders

- <doc:Borders-Border>
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
2 changes: 1 addition & 1 deletion Sources/Slipstream/TailwindCSS/Borders/View+border.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 31 additions & 0 deletions Sources/Slipstream/TailwindCSS/Size.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 2 additions & 1 deletion Tests/SlipstreamTests/Sites/CatalogSiteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -73,7 +74,7 @@ struct CatalogSiteTests {
<link rel="stylesheet" href="/css/bootstrap.css" />
</head>
<body id="root">
<div class="container border-b-4 border-black dark:text-red-800 lg:px-12 duration-300 ease-in-out">
<div class="container border-b-4 border-black bg-[length:50px_100px] bg-[url('/logo.svg')] bg-no-repeat dark:text-red-800 lg:px-12 duration-300 ease-in-out">
Hello
<br />world!
<a href="/about">About</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -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"))) == #"<div class="bg-[url('/logo.png')]"></div>"#)
}

@Test func withRepeat() throws {
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .no)) == #"<div class="bg-[url('/logo.png')] bg-no-repeat"></div>"#)
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .yes)) == #"<div class="bg-[url('/logo.png')] bg-repeat"></div>"#)
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .x)) == #"<div class="bg-[url('/logo.png')] bg-repeat-x"></div>"#)
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .y)) == #"<div class="bg-[url('/logo.png')] bg-repeat-y"></div>"#)
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .round)) == #"<div class="bg-[url('/logo.png')] bg-repeat-round"></div>"#)
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), repeat: .space)) == #"<div class="bg-[url('/logo.png')] bg-repeat-space"></div>"#)
}

@Test func withSize() throws {
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), size: .auto)) == #"<div class="bg-[url('/logo.png')] bg-auto"></div>"#)
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), size: .contain)) == #"<div class="bg-[url('/logo.png')] bg-contain"></div>"#)
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), size: .cover)) == #"<div class="bg-[url('/logo.png')] bg-cover"></div>"#)
try #expect(renderHTML(Div {}.backgroundImage(URL(string: "/logo.png"), size: .size(width: 100, height: 50))) == #"<div class="bg-[length:100px_50px] bg-[url('/logo.png')]"></div>"#)
}
}

0 comments on commit 96fbd5a

Please sign in to comment.