Skip to content

Commit

Permalink
Introduce decorate background element
Browse files Browse the repository at this point in the history
  • Loading branch information
kyleve committed Nov 5, 2020
1 parent 953d48c commit 255fce9
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 5 deletions.
185 changes: 185 additions & 0 deletions BlueprintUI/Sources/Layout/DecorateBackground.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
//
// DecorateBackground.swift
// BlueprintUI
//
// Created by Kyle Van Essen on 11/4/20.
//

import UIKit


///
/// Places a background behind the given `wrapped` element,
/// and overflows it by the amount specified in `overflow`.
///
/// The size of the element is determined only by the `wrapped` element –
/// that is, the `background` element will overflow the bounds of the element, but not
/// affect its layout or size. This is similar to how UIKit handles shadows: They live outside of the layout
/// rect of the view:
///
/// The arrows represent the measured size of the view for layout purposes.
/// ```
/// ┌───────────────────┐
/// │ Background │
/// │ ┏━━━━━━━━━━━━━━━┓ │ ▲
/// │ ┃ ┃ │ │
/// │ ┃ Wrapped ┃ │ │
/// │ ┃ ┃ │ │
/// │ ┗━━━━━━━━━━━━━━━┛ │ ▼
/// └───────────────────┘
/// ◀───────────────▶
/// ```
///
/// This is useful to render on-touch or selected states of elements, where you
/// want to provide a padded background that does otherwise affect the layout of the element,
/// which is likely controlled by its parent element.
///
public struct DecorateBackground : Element {

/// The element which provides the sizing and measurement.
public var wrapped : Element

/// The element which is used to render the background.
/// It is stretched to fit the `wrapped` content, plus the `overflow` padding.
///
/// If you have a 100w x 50h element, and an overflow of (10, 10, 10, 10),
/// the measured sized will be 100w x 50h, and the background will be
/// sized to be 120w x 70h.
/// ```
/// ┌───────────────────┐
/// │ ┏━━━━━━━━━━━━━━━┓ │ ▲
/// │ ┃ ┃ │ │
/// │ ┃ Wrapped ┃ │ │
/// │ ┃ ┃ │ │
/// │ ┗━━━━━━━━━━━━━━━┛ │ ▼
/// └───────────────────┘
/// ◀───────────────▶
/// ```
public var background : Element

/// How much the background should overflow the measured bounds of the
/// element. Positive values overflow outside of the bounds, and negative
/// values underflow to inside the bounds.
public var overflow : UIEdgeInsets

/// Creates a new instance with the provided overflow, background, and wrapped element.
public init(
overflow: UIEdgeInsets,
background: () -> Element,
wrapping: () -> Element
) {
self.wrapped = wrapping()
self.background = background()
self.overflow = overflow
}

/// Creates a new instance with the provided uniform overflow, background, and wrapped element.
public init(
uniform: CGFloat,
background: () -> Element,
wrapping: () -> Element
) {
self.init(
overflow: UIEdgeInsets(top: uniform, left: uniform, bottom: uniform, right: uniform),
background: background,
wrapping: wrapping
)
}

/// Creates a new instance with the provided horizontal and vertical overflow, background, and wrapped element.
public init(
horizontal: CGFloat? = nil,
vertical : CGFloat? = nil,
background: () -> Element,
wrapping: () -> Element
) {
self.init(
overflow: UIEdgeInsets(top: vertical ?? 0, left: horizontal ?? 0, bottom: vertical ?? 0, right: horizontal ?? 0),
background: background,
wrapping: wrapping
)
}

// MARK: Element

public var content: ElementContent {
let layout = Layout(overflow: self.overflow)

return ElementContent(layout: layout) { builder in
builder.add(element: self.background)
builder.add(element: self.wrapped)
}
}

public func backingViewDescription(bounds: CGRect, subtreeExtent: CGRect?) -> ViewDescription? {
nil
}
}


extension Element {

/// Decorates the element with the provided background for the provided overflow.
public func decorateBackground(
with overflow : UIEdgeInsets,
background : () -> Element
) -> Element {
DecorateBackground(overflow: overflow, background: background, wrapping: { self })
}

/// Decorates the element with the provided background for the provided uniform overflow.
public func decorateBackground(
with uniform : CGFloat,
background : () -> Element
) -> Element {
DecorateBackground(uniform: uniform, background: background, wrapping: { self })
}

/// Decorates the element with the provided background for the provided horizontal and vertical overflow.
public func decorateBackground(
horizontal: CGFloat? = nil,
vertical : CGFloat? = nil,
background : () -> Element
) -> Element {
DecorateBackground(horizontal: horizontal, vertical: vertical, background: background, wrapping: { self })
}
}


extension DecorateBackground {
fileprivate struct Layout : BlueprintUI.Layout {

var overflow : UIEdgeInsets

func measure(in constraint: SizeConstraint, items: [(traits: (), content: Measurable)]) -> CGSize {

precondition(items.count == 2)

let wrapped = items[1]

return wrapped.content.measure(in: constraint)
}

func layout(size: CGSize, items: [(traits: (), content: Measurable)]) -> [LayoutAttributes] {

precondition(items.count == 2)

return [
LayoutAttributes(frame: CGRect(origin: .zero, size: size).inset(by: self.overflow.inverted)),
LayoutAttributes(size: size)
]
}
}
}


extension UIEdgeInsets {
fileprivate var inverted : UIEdgeInsets {
UIEdgeInsets(
top: -self.top,
left: -self.left,
bottom: -self.bottom,
right: -self.right
)
}
}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- [Introduce `DecorateBackground`](https://github.com/square/Blueprint/pull/178) to allow placing a background behind an `Element`, without affecting its layout. This is useful for rendering tap or selection states which should overflow the natural bounds of the `Element`, similar to a shadow.

### Removed

### Changed
Expand Down
14 changes: 9 additions & 5 deletions SampleApp/Sources/RootViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,26 @@ fileprivate struct DemoItem : ProxyElement
var onTap : () -> ()

var elementRepresentation: Element {
Label(text: self.title) { label in

Label(text: self.title) { label in
label.font = .systemFont(ofSize: 18.0, weight: .semibold)
}
.inset(uniform: 20.0)
.box(
background: .white,
corners: .rounded(radius: 20.0),
corners: .rounded(radius: 15.0),
shadow: .simple(
radius: 6.0,
opacity: 0.2,
offset: .init(width: 0, height: 3.0),
radius: 5.0,
opacity: 0.3,
offset: .init(width: 0, height: 2.0),
color: .black
)
)
.tappable {
self.onTap()
}
.decorateBackground(with: 5.0) {
Box(backgroundColor: .init(white: 0.0, alpha: 0.1), cornerStyle: .rounded(radius: 17))
}
}
}

0 comments on commit 255fce9

Please sign in to comment.