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

Aligned element #21

Merged
merged 3 commits into from
Oct 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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
99 changes: 99 additions & 0 deletions BlueprintUI/Sources/Layout/Aligned.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/// Aligns a content element within itself. The vertical and horizontal alignment may be set independently.
///
/// The size of the content element is determined by calling `measure(in:)` on
/// the content element – even if that size is larger than the wrapping element.
///
public struct Aligned: Element {
/// The possible vertical alignment values.
public enum VerticalAlignment {
/// Aligns the content to the top edge of the containing element.
case top
/// Centers the content vertically.
case center
/// Aligns the content to the bottom edge of the containing element.
case bottom
}

/// The possible horizontal alignment values.
public enum HorizontalAlignment {
/// Aligns the content to the leading edge of the containing element.
/// In left-to-right languages, this is the left edge.
case leading
/// Centers the content horizontally.
case center
/// Aligns the content to the trailing edge of the containing element.
/// In left-to-right languages, this is the right edge.
case trailing
}

/// The content element to be aligned.
public var wrappedElement: Element
/// The vertical alignment.
public var verticalAlignment: VerticalAlignment
/// The horizontal alignment.
public var horizontalAlignment: HorizontalAlignment

/// Initializes an `Aligned` with the given content element and alignments.
///
/// - parameters:
/// - vertically: The vertical alignment. Defaults to centered.
/// - horizontally: The horizontal alignment. Defaults to centered.
/// - wrapping: The content element to be aligned.
init(
vertically verticalAlignment: VerticalAlignment = .center,
horizontally horizontalAlignment: HorizontalAlignment = .center,
wrapping wrappedElement: Element
) {
self.verticalAlignment = verticalAlignment
self.horizontalAlignment = horizontalAlignment
self.wrappedElement = wrappedElement
}

public var content: ElementContent {
let layout = Layout(verticalAlignment: verticalAlignment, horizontalAlignment: horizontalAlignment)
return ElementContent(child: wrappedElement, layout: layout)
}

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

private struct Layout: SingleChildLayout {
var verticalAlignment: VerticalAlignment
var horizontalAlignment: HorizontalAlignment

func measure(in constraint: SizeConstraint, child: Measurable) -> CGSize {
return child.measure(in: constraint)
}

func layout(size: CGSize, child: Measurable) -> LayoutAttributes {
let contentSize = child.measure(in: SizeConstraint(size))

var attributes = LayoutAttributes(size: contentSize)

switch verticalAlignment {
case .top:
attributes.frame.origin.y = 0
case .center:
attributes.frame.origin.y = (size.height - contentSize.height) / 2.0
case .bottom:
attributes.frame.origin.y = size.height - contentSize.height
}

switch horizontalAlignment {
case .leading:
attributes.frame.origin.x = 0
case .center:
attributes.frame.origin.x = (size.width - contentSize.width) / 2.0
case .trailing:
attributes.frame.origin.x = size.width - contentSize.width
}

// TODO: screen-scale round here once that lands
attributes.frame.origin.x.round()
attributes.frame.origin.y.round()

return attributes
}
}
}
28 changes: 3 additions & 25 deletions BlueprintUI/Sources/Layout/Centered.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/// the content element – even if that size is larger than the wrapping `Centered`
/// element.
///
public struct Centered: Element {
public struct Centered: ProxyElement {

/// The content element to be centered.
public var wrappedElement: Element
Expand All @@ -14,29 +14,7 @@ public struct Centered: Element {
self.wrappedElement = wrappedElement
}

public var content: ElementContent {
return ElementContent(child: wrappedElement, layout: Layout())
}

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

extension Centered {
fileprivate struct Layout: SingleChildLayout {

func measure(in constraint: SizeConstraint, child: Measurable) -> CGSize {
return child.measure(in: constraint)
}

func layout(size: CGSize, child: Measurable) -> LayoutAttributes {
var childAttributes = LayoutAttributes()
childAttributes.bounds.size = child.measure(in: SizeConstraint(size))
childAttributes.center.x = size.width/2.0
childAttributes.center.y = size.height/2.0
return childAttributes
}

public var elementRepresentation: Element {
return Aligned(vertically: .center, horizontally: .center, wrapping: wrappedElement)
}
}
101 changes: 101 additions & 0 deletions BlueprintUI/Tests/AlignedTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import XCTest
@testable import BlueprintUI


class AlignedTests: XCTestCase {

let testSize = CGSize(width: 100, height: 200)
let layoutFrame = CGRect(x: 0, y: 0, width: 5000, height: 6000)

private func childLayoutResultNodesAligned(
horizontally: Aligned.HorizontalAlignment = .center,
vertically: Aligned.VerticalAlignment = .center
) -> [LayoutResultNode] {
let content = TestElement(size: testSize)
let element = Aligned(vertically: vertically, horizontally: horizontally, wrapping: content)
let children = element
.layout(frame: layoutFrame)
.children
.map { $0.node }
return children
}

func test_measuring() {
let constraint = SizeConstraint(width: .unconstrained, height: .unconstrained)
let content = TestElement(size: testSize)
let element = Aligned(wrapping: content)
XCTAssertEqual(element.content.measure(in: constraint), content.content.measure(in: constraint))
}

func test_horizontalLeading() {
let children = childLayoutResultNodesAligned(horizontally: .leading)

XCTAssertEqual(children.count, 1)
let frame = children[0].layoutAttributes.frame
XCTAssertEqual(frame.minX, 0)
XCTAssertEqual(frame.maxX, 100)
XCTAssertTrue(children[0].element is TestElement)
}

func test_horizontalCenter() {
let children = childLayoutResultNodesAligned(horizontally: .center)

XCTAssertEqual(children.count, 1)
let frame = children[0].layoutAttributes.frame
XCTAssertEqual(frame.minX, 2450)
XCTAssertEqual(frame.maxX, 2550)
XCTAssertTrue(children[0].element is TestElement)
}

func test_horizontalTrailing() {
let children = childLayoutResultNodesAligned(horizontally: .trailing)

XCTAssertEqual(children.count, 1)
let frame = children[0].layoutAttributes.frame
XCTAssertEqual(frame.minX, 4900)
XCTAssertEqual(frame.maxX, 5000)
XCTAssertTrue(children[0].element is TestElement)
}

func test_verticalTop() {
let children = childLayoutResultNodesAligned(vertically: .top)

XCTAssertEqual(children.count, 1)
let frame = children[0].layoutAttributes.frame
XCTAssertEqual(frame.minY, 0)
XCTAssertEqual(frame.maxY, 200)
XCTAssertTrue(children[0].element is TestElement)
}

func test_verticalCenter() {
let children = childLayoutResultNodesAligned(vertically: .center)

XCTAssertEqual(children.count, 1)
let frame = children[0].layoutAttributes.frame
XCTAssertEqual(frame.minY, 2900)
XCTAssertEqual(frame.maxY, 3100)
XCTAssertTrue(children[0].element is TestElement)
}

func test_verticalBottom() {
let children = childLayoutResultNodesAligned(vertically: .bottom)

XCTAssertEqual(children.count, 1)
let frame = children[0].layoutAttributes.frame
XCTAssertEqual(frame.minY, 5800)
XCTAssertEqual(frame.maxY, 6000)
XCTAssertTrue(children[0].element is TestElement)
}
}

private struct TestElement: Element {
let size: CGSize

var content: ElementContent {
return ElementContent(intrinsicSize: size)
}

func backingViewDescription(bounds: CGRect, subtreeExtent: CGRect?) -> ViewDescription? {
return nil
}
}
33 changes: 24 additions & 9 deletions BlueprintUI/Tests/CenteredTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,32 @@ class CenteredTests: XCTestCase {
let element = TestElement()
let centered = Centered(element)

let children = centered
.layout(frame: CGRect(x: 0, y: 0, width: 5000, height: 6000))
.children
.map { $0.node }

XCTAssertEqual(children.count, 1)
XCTAssertEqual(children[0].layoutAttributes.center, CGPoint(x: 2500, y: 3000))
XCTAssertEqual(children[0].layoutAttributes.bounds, CGRect(x: 0, y: 0, width: 123, height: 456))
XCTAssertTrue(children[0].element is TestElement)
let layout = centered.layout(frame: CGRect(x: 0, y: 0, width: 5000, height: 6000))
if let child = findLayout(of: TestElement.self, in: layout) {
XCTAssertEqual(
child.layoutAttributes.frame,
CGRect(
x: 2439,
y: 2772,
width: 123,
height: 456))
} else {
XCTFail("TestElement should be a child element")
}
}

private func findLayout(of elementType: Element.Type, in node: LayoutResultNode) -> LayoutResultNode? {
if type(of: node.element) == elementType {
return node
}

for child in node.children {
if let node = findLayout(of: elementType, in: child.node) {
return node
}
}
return nil
}
}


Expand Down
13 changes: 12 additions & 1 deletion Documentation/GettingStarted/Layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,18 @@ A `Centered` element always wraps a single child. During a layout pass, the layo
After `Centered` has been assigned a size during a layout pass, it always sizes the wrapped element to its measured size, then centers it within the layout area.

```swift
let centered = Centered(wrapping: Label(text: "Hello, world"))
let centered = Centered(Label(text: "Hello, world"))
```

### `Aligned`

Aligns a single child horizontally and vertically to the left (leading edge), right (trailing edge), top, bottom, or center of the available space. Like `Centered`, it delegates measuring to the child.

```swift
let aligned = Aligned(
vertically: .bottom,
horizontally: .trailing,
wrapping: Label(text: "Hello from the corner"))
```

### `Spacer`
Expand Down