Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Decorate element #178

Merged
merged 7 commits into from
Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
kyleve marked this conversation as resolved.
Show resolved Hide resolved

/// Creates a new instance with the provided overflow, background, and wrapped element.
public init(
overflow: UIEdgeInsets,
background: () -> Element,
kyleve marked this conversation as resolved.
Show resolved Hide resolved
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]
kyleve marked this conversation as resolved.
Show resolved Hide resolved

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))
}
}
}