Add List and related Views (#147)

* List and Divider

* Add DisclosureGroup and OutlineGroup

* Add aria attributes

* OutlineGroup List initializers

* Make only chevron clickable

* ListStyle

* Fix line lengths

* Fix demo

* Section

* Modify progressmd

* Remove useless comment

* Switch to hr element

* Disable Divider for last row

* Make list and outline style defaults constant

* Minor cleanup

* ListStyleDeferredToRenderer

* Fix demo
This commit is contained in:
Carson Katri 2020-07-06 15:46:59 -04:00 committed by GitHub
parent aedab8ca05
commit 50be7b16f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1365 additions and 7 deletions

View File

@ -0,0 +1,53 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/3/20.
//
// FIXME: Make `Animatable`
public protocol GeometryEffect: ViewModifier {
func effectValue(size: CGSize) -> ProjectionTransform
}
public struct ProjectionTransform: Equatable {
public var m11: CGFloat = 1, m12: CGFloat = 0, m13: CGFloat = 0
public var m21: CGFloat = 0, m22: CGFloat = 1, m23: CGFloat = 0
public var m31: CGFloat = 0, m32: CGFloat = 0, m33: CGFloat = 1
public init() {}
public init(_ m: CGAffineTransform) {
m11 = m.a
m12 = m.b
m21 = m.c
m22 = m.d
m31 = m.tx
m32 = m.ty
}
public var isIdentity: Bool {
self == ProjectionTransform()
}
public var isAffine: Bool {
m13 == 0 && m23 == 0 && m33 == 1
}
public mutating func invert() -> Bool {
self = inverted()
return true
}
public func inverted() -> ProjectionTransform {
.init(CGAffineTransform(a: m11, b: m12, c: m21, d: m22, tx: m31, ty: m32).inverted())
}
}

View File

@ -0,0 +1,40 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/3/20.
//
public struct _RotationEffect: GeometryEffect {
public var angle: Angle
public var anchor: UnitPoint
public init(angle: Angle, anchor: UnitPoint = .center) {
self.angle = angle
self.anchor = anchor
}
public func effectValue(size: CGSize) -> ProjectionTransform {
.init(CGAffineTransform.identity.rotated(by: angle.radians))
}
public func body(content: Content) -> some View {
content
}
}
extension View {
public func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View {
modifier(_RotationEffect(angle: angle, anchor: anchor))
}
}

View File

@ -24,10 +24,14 @@ public struct ModifiedContent<Content, Modifier> {
}
}
extension ModifiedContent: View where Content: View, Modifier: ViewModifier {
extension ModifiedContent: View, ParentView where Content: View, Modifier: ViewModifier {
public var body: Body {
neverBody("ModifiedContent<View, ViewModifier>")
}
public var children: [AnyView] {
[AnyView(content)]
}
}
extension ModifiedContent: ViewModifier where Content: ViewModifier, Modifier: ViewModifier {

View File

@ -0,0 +1,88 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/5/20.
//
public protocol ListStyle {}
/// A protocol implemented on the renderer to create platform-specific list styles.
public protocol ListStyleDeferredToRenderer {
func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View
func listRow<Row>(_ row: Row) -> AnyView where Row: View
func sectionHeader<Header>(_ header: Header) -> AnyView where Header: View
func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View
func sectionFooter<Footer>(_ footer: Footer) -> AnyView where Footer: View
}
public extension ListStyleDeferredToRenderer {
func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View {
AnyView(content)
}
func listRow<Row>(_ row: Row) -> AnyView where Row: View {
AnyView(row
.padding([.trailing, .top, .bottom]))
}
func sectionHeader<Header>(_ header: Header) -> AnyView where Header: View {
AnyView(header)
}
func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View {
AnyView(section)
}
func sectionFooter<Footer>(_ footer: Footer) -> AnyView where Footer: View {
AnyView(footer)
}
}
public typealias DefaultListStyle = PlainListStyle
public struct PlainListStyle: ListStyle {
public init() {}
}
public struct GroupedListStyle: ListStyle {
public init() {}
}
public struct InsetListStyle: ListStyle {
public init() {}
}
public struct InsetGroupedListStyle: ListStyle {
public init() {}
}
enum ListStyleKey: EnvironmentKey {
static let defaultValue: ListStyle = DefaultListStyle()
}
extension EnvironmentValues {
var listStyle: ListStyle {
get {
self[ListStyleKey.self]
}
set {
self[ListStyleKey.self] = newValue
}
}
}
extension View {
public func listStyle<S>(_ style: S) -> some View where S: ListStyle {
environment(\.listStyle, style)
}
}

View File

@ -0,0 +1,47 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/4/20.
//
public protocol _OutlineGroupStyle {}
public struct _DefaultOutlineGroupStyle: _OutlineGroupStyle {
public init() {}
}
public struct _ListOutlineGroupStyle: _OutlineGroupStyle {
public init() {}
}
enum _OutlineGroupStyleKey: EnvironmentKey {
static let defaultValue: _OutlineGroupStyle = _DefaultOutlineGroupStyle()
}
extension EnvironmentValues {
var _outlineGroupStyle: _OutlineGroupStyle {
get {
self[_OutlineGroupStyleKey.self]
}
set {
self[_OutlineGroupStyleKey.self] = newValue
}
}
}
extension View {
func outlineGroupStyle(_ style: _OutlineGroupStyle) -> some View {
environment(\._outlineGroupStyle, style)
}
}

View File

@ -74,3 +74,12 @@ extension AnyView: ParentView {
(view as? ParentView)?.children ?? []
}
}
public struct _AnyViewProxy {
public var subject: AnyView
public init(_ subject: AnyView) { self.subject = subject }
public var type: Any.Type { subject.type }
public var view: Any { subject.view }
}

View File

@ -0,0 +1,87 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/3/20.
//
public struct DisclosureGroup<Label, Content>: View
where Label: View, Content: View {
@State var isExpanded: Bool = false
let isExpandedBinding: Binding<Bool>?
@Environment(\._outlineGroupStyle) var style: _OutlineGroupStyle
let label: Label
let content: () -> Content
public init(@ViewBuilder content: @escaping () -> Content,
@ViewBuilder label: () -> Label) {
isExpandedBinding = nil
self.label = label()
self.content = content
}
public init(isExpanded: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content,
@ViewBuilder label: () -> Label) {
isExpandedBinding = isExpanded
self.label = label()
self.content = content
}
public var body: Never {
neverBody("DisclosureGroup")
}
}
extension DisclosureGroup where Label == Text {
// FIXME: Implement LocalizedStringKey
// public init(_ titleKey: LocalizedStringKey,
// @ViewBuilder content: @escaping () -> Content)
// public init(_ titleKey: SwiftUI.LocalizedStringKey,
// isExpanded: SwiftUI.Binding<Swift.Bool>,
// @SwiftUI.ViewBuilder content: @escaping () -> Content)
@_disfavoredOverload public init<S>(_ label: S,
@ViewBuilder content: @escaping () -> Content)
where S: StringProtocol {
self.init(content: content, label: { Text(label) })
}
@_disfavoredOverload public init<S>(_ label: S,
isExpanded: Binding<Bool>,
@ViewBuilder content: @escaping () -> Content)
where S: StringProtocol {
self.init(isExpanded: isExpanded, content: content, label: { Text(label) })
}
}
public struct _DisclosureGroupProxy<Label, Content>
where Label: View, Content: View {
public var subject: DisclosureGroup<Label, Content>
public init(_ subject: DisclosureGroup<Label, Content>) { self.subject = subject }
public var label: Label { subject.label }
public var content: () -> Content { subject.content }
public var style: _OutlineGroupStyle { subject.style }
public var isExpanded: Bool {
subject.isExpandedBinding?.wrappedValue ?? subject.isExpanded
}
public func toggleIsExpanded() {
subject.isExpandedBinding?.wrappedValue.toggle()
subject.isExpanded.toggle()
}
}

View File

@ -0,0 +1,24 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/2/20.
//
/// A horizontal line for separating content.
public struct Divider: View {
public init() {}
public var body: Never {
neverBody("Divider")
}
}

View File

@ -0,0 +1,394 @@
// Copyright 2018-2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/2/20.
//
public struct List<SelectionValue, Content>: View
where SelectionValue: Hashable, Content: View {
public enum _Selection {
case one(Binding<SelectionValue?>?)
case many(Binding<Set<SelectionValue>>?)
}
let selection: _Selection
let content: Content
@Environment(\.listStyle) var style: ListStyle
public init(selection: Binding<Set<SelectionValue>>?, @ViewBuilder content: () -> Content) {
self.selection = .many(selection)
self.content = content()
}
public init(selection: Binding<SelectionValue?>?, @ViewBuilder content: () -> Content) {
self.selection = .one(selection)
self.content = content()
}
var listStack: some View {
VStack(alignment: .leading) { () -> AnyView in
if let contentContainer = content as? ParentView {
var sections = [AnyView]()
var currentSection = [AnyView]()
for child in contentContainer.children {
if child.view is SectionView {
if currentSection.count > 0 {
sections.append(AnyView(Section {
ForEach(Array(currentSection.enumerated()), id: \.offset) { _, view in
view
}
}))
currentSection = []
}
sections.append(child)
} else {
if child.children.count > 0 {
currentSection.append(contentsOf: child.children)
} else {
currentSection.append(child)
}
}
}
if currentSection.count > 0 {
sections.append(AnyView(Section {
ForEach(Array(currentSection.enumerated()), id: \.offset) { _, view in
view
}
}))
}
return AnyView(_ListRow.buildItems(sections) { (view, isLast) -> AnyView in
if let section = view.view as? SectionView {
return AnyView(section.listRow(style))
} else {
return AnyView(_ListRow.listRow(view, style, isLast: isLast))
}
})
} else {
return AnyView(content)
}
}
}
public var body: some View {
if let style = style as? ListStyleDeferredToRenderer {
return style.listBody(ScrollView {
HStack {
Spacer()
}
listStack
.environment(\._outlineGroupStyle, _ListOutlineGroupStyle())
})
} else {
return AnyView(ScrollView {
HStack {
Spacer()
}
listStack
.environment(\._outlineGroupStyle, _ListOutlineGroupStyle())
})
}
}
}
public struct _ListRow {
static func buildItems<RowView>(_ children: [AnyView],
@ViewBuilder rowView: @escaping (AnyView, Bool) -> RowView)
-> some View where RowView: View {
ForEach(Array(children.enumerated()), id: \.offset) { offset, view in
VStack(alignment: .leading) {
HStack {
Spacer()
}
AnyView(rowView(view, offset == children.count - 1))
}
}
}
@ViewBuilder
public static func listRow<V: View>(_ view: V, _ style: ListStyle, isLast: Bool) -> some View {
(style as? ListStyleDeferredToRenderer)?.listRow(view) ??
AnyView(view.padding([.trailing, .top, .bottom]))
if !isLast {
Divider()
}
}
}
/// This is a helper class that works around absence of "package private" access control in Swift
public struct _ListProxy<SelectionValue, Content>
where SelectionValue: Hashable, Content: View {
public let subject: List<SelectionValue, Content>
public init(_ subject: List<SelectionValue, Content>) {
self.subject = subject
}
public var content: Content { subject.content }
public var selection: List<SelectionValue, Content>._Selection { subject.selection }
}
extension List {
// - MARK: Collection initializers
public init<Data, RowContent>(_ data: Data,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View,
Data.Element: Identifiable {
self.init(selection: selection) {
ForEach(data) { row in
HStack {
rowContent(row)
}
}
}
}
public init<Data, ID, RowContent>(_ data: Data,
id: KeyPath<Data.Element, ID>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection,
ID: Hashable, RowContent: View {
self.init(selection: selection) {
ForEach(data, id: id) { row in
HStack {
rowContent(row)
}
}
}
}
public init<Data, ID, RowContent>(_ data: Data,
id: KeyPath<Data.Element, ID>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection, ID: Hashable, RowContent: View {
self.init(selection: selection) {
ForEach(data, id: id) { row in
HStack {
rowContent(row)
}
}
}
}
public init<Data, RowContent>(_ data: Data,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable {
self.init(selection: selection) {
ForEach(data) { row in
HStack {
rowContent(row)
}
}
}
}
// - MARK: Range initializers
public init<RowContent>(_ data: Range<Int>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Int) -> RowContent)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View {
self.init(selection: selection) {
ForEach(data) { row in
HStack {
rowContent(row)
}
}
}
}
public init<RowContent>(_ data: Range<Int>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Int) -> RowContent)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View {
self.init(selection: selection) {
ForEach(data) { row in
HStack {
rowContent(row)
}
}
}
}
// - MARK: OutlineGroup initializers
public init<Data, RowContent>(_ data: Data,
children: KeyPath<Data.Element, Data?>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == OutlineGroup<Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>>,
Data: RandomAccessCollection,
RowContent: View,
Data.Element: Identifiable {
self.init(selection: selection) {
OutlineGroup(data, children: children) { row in
HStack {
rowContent(row)
}
}
}
}
public init<Data, ID, RowContent>(_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
selection: Binding<Set<SelectionValue>>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == OutlineGroup<Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>>,
Data: RandomAccessCollection,
ID: Hashable,
RowContent: View {
self.init(selection: selection) {
OutlineGroup(data, id: id, children: children) { row in
HStack {
rowContent(row)
}
}
}
}
public init<Data, RowContent>(_ data: Data,
children: KeyPath<Data.Element, Data?>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == OutlineGroup<Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>>,
Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable {
self.init(selection: selection) {
OutlineGroup(data, children: children) { row in
HStack {
rowContent(row)
}
}
}
}
public init<Data, ID, RowContent>(_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
selection: Binding<SelectionValue?>?,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == OutlineGroup<Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>>,
Data: RandomAccessCollection, ID: Hashable, RowContent: View {
self.init(selection: selection) {
OutlineGroup(data, id: id, children: children) { row in
HStack {
rowContent(row)
}
}
}
}
}
extension List where SelectionValue == Never {
public init(@ViewBuilder content: () -> Content) {
selection = .one(nil)
self.content = content()
}
public init<Data, RowContent>(_ data: Data,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>,
Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable {
selection = .one(nil)
content = ForEach(data) { row in
HStack {
rowContent(row)
}
}
}
public init<Data, RowContent>(_ data: Data,
children: KeyPath<Data.Element, Data?>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == OutlineGroup<Data,
Data.Element.ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>>,
Data: RandomAccessCollection, RowContent: View, Data.Element: Identifiable {
self.init {
OutlineGroup(data, children: children) { row in
HStack {
rowContent(row)
}
}
}
}
public init<Data, ID, RowContent>(_ data: Data,
id: KeyPath<Data.Element, ID>,
children: KeyPath<Data.Element, Data?>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == OutlineGroup<Data,
ID,
HStack<RowContent>,
HStack<RowContent>,
DisclosureGroup<HStack<RowContent>, OutlineSubgroupChildren>>,
Data: RandomAccessCollection, ID: Hashable, RowContent: View {
self.init {
OutlineGroup(data, id: id, children: children) { row in
HStack {
rowContent(row)
}
}
}
}
public init<Data, ID, RowContent>(_ data: Data,
id: KeyPath<Data.Element, ID>,
@ViewBuilder rowContent: @escaping (Data.Element) -> RowContent)
where Content == ForEach<Data, ID, HStack<RowContent>>,
Data: RandomAccessCollection, ID: Hashable, RowContent: View {
selection = .one(nil)
content = ForEach(data, id: id) { row in
HStack {
rowContent(row)
}
}
}
public init<RowContent>(_ data: Range<Int>,
@ViewBuilder rowContent: @escaping (Int) -> RowContent)
where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent: View {
selection = .one(nil)
content = ForEach(data) { row in
HStack {
rowContent(row)
}
}
}
}

View File

@ -0,0 +1,125 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/3/20.
//
public struct OutlineGroup<Data, ID, Parent, Leaf, Subgroup>
where Data: RandomAccessCollection, ID: Hashable {
enum Root {
case collection(Data)
case single(Data.Element)
}
let root: Root
let children: KeyPath<Data.Element, Data?>
let id: KeyPath<Data.Element, ID>
let content: (Data.Element) -> Leaf
}
extension OutlineGroup where ID == Data.Element.ID,
Parent: View,
Parent == Leaf,
Subgroup == DisclosureGroup<Parent, OutlineSubgroupChildren>,
Data.Element: Identifiable {
public init<DataElement>(_ root: DataElement,
children: KeyPath<DataElement, Data?>,
@ViewBuilder content: @escaping (DataElement) -> Leaf)
where ID == DataElement.ID, DataElement: Identifiable, DataElement == Data.Element {
self.init(root,
id: \.id,
children: children,
content: content)
}
public init<DataElement>(_ data: Data,
children: KeyPath<DataElement, Data?>,
@ViewBuilder content: @escaping (DataElement) -> Leaf)
where ID == DataElement.ID,
DataElement: Identifiable,
DataElement == Data.Element {
self.init(data,
id: \.id,
children: children,
content: content)
}
}
extension OutlineGroup where Parent: View,
Parent == Leaf,
Subgroup == DisclosureGroup<Parent, OutlineSubgroupChildren> {
public init<DataElement>(_ root: DataElement,
id: KeyPath<DataElement, ID>,
children: KeyPath<DataElement, Data?>,
@ViewBuilder content: @escaping (DataElement) -> Leaf)
where DataElement == Data.Element {
self.root = .single(root)
self.children = children
self.id = id
self.content = content
}
public init<DataElement>(_ data: Data,
id: KeyPath<DataElement, ID>,
children: KeyPath<DataElement, Data?>,
@ViewBuilder content: @escaping (DataElement) -> Leaf)
where DataElement == Data.Element {
root = .collection(data)
self.id = id
self.children = children
self.content = content
}
}
extension OutlineGroup: View where Parent: View, Leaf: View, Subgroup: View {
public var body: some View {
switch root {
case let .collection(data):
return AnyView(ForEach(data, id: id) { elem in
OutlineSubgroupChildren { () -> AnyView in
if let subgroup = elem[keyPath: children] {
return AnyView(DisclosureGroup(content: {
OutlineGroup(root: .collection(subgroup),
children: children,
id: id,
content: content)
}) {
content(elem)
})
} else {
return AnyView(content(elem))
}
}
})
case let .single(root):
return AnyView(DisclosureGroup(content: {
if let subgroup = root[keyPath: children] {
OutlineGroup(root: .collection(subgroup), children: children, id: id, content: content)
} else {
content(root)
}
}) {
content(root)
})
}
}
}
public struct OutlineSubgroupChildren: View {
let children: () -> AnyView
public var body: some View {
children()
}
}

View File

@ -0,0 +1,109 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/5/20.
//
protocol SectionView {
func listRow(_ style: ListStyle) -> AnyView
}
public struct Section<Parent, Content, Footer> {
let header: Parent
let footer: Footer
let content: Content
}
extension Section: View, SectionView where Parent: View, Content: View, Footer: View {
public init(header: Parent, footer: Footer, @ViewBuilder content: () -> Content) {
self.header = header
self.footer = footer
self.content = content()
}
@ViewBuilder
public var body: TupleView<(Parent, Content, Footer)> {
header
content
footer
}
func sectionContent(_ style: ListStyle) -> AnyView {
if let contentContainer = content as? ParentView {
let rows = _ListRow.buildItems(contentContainer.children) { view, isLast in
_ListRow.listRow(view, style, isLast: isLast)
}
if let style = style as? ListStyleDeferredToRenderer {
return style.sectionBody(rows)
} else {
return AnyView(rows)
}
} else if let style = style as? ListStyleDeferredToRenderer {
return style.sectionBody(content)
} else {
return AnyView(content)
}
}
func footerView(_ style: ListStyle) -> AnyView {
if footer is EmptyView {
return AnyView(EmptyView())
} else if let style = style as? ListStyleDeferredToRenderer {
return style.sectionFooter(footer)
} else {
return AnyView(_ListRow.listRow(footer, style, isLast: true))
}
}
func headerView(_ style: ListStyle) -> AnyView {
if header is EmptyView {
return AnyView(EmptyView())
} else if let style = style as? ListStyleDeferredToRenderer {
return style.sectionHeader(header)
} else {
return AnyView(header)
}
}
func listRow(_ style: ListStyle) -> AnyView {
AnyView(VStack(alignment: .leading) {
headerView(style)
sectionContent(style)
footerView(style)
})
}
}
extension Section where Parent == EmptyView, Content: View, Footer: View {
public init(footer: Footer, @ViewBuilder content: () -> Content) {
self.init(header: EmptyView(), footer: footer, content: content)
}
}
extension Section where Parent: View, Content: View, Footer == EmptyView {
public init(header: Parent, @ViewBuilder content: () -> Content) {
self.init(header: header, footer: EmptyView(), content: content)
}
}
extension Section where Parent == EmptyView, Content: View, Footer == EmptyView {
public init(@ViewBuilder content: () -> Content) {
self.init(header: EmptyView(), footer: EmptyView(), content: content)
}
}
// FIXME: Implement IsCollapsibleTraitKey (and TraitKeys)
// extension Section where Parent : View, Content : View, Footer : View {
// public func collapsible(_ collapsible: Bool) -> some View
// }

View File

@ -0,0 +1,24 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/3/20.
//
import TokamakCore
extension _RotationEffect: DOMViewModifier {
public var attributes: [String: String] {
["style": "transform: rotate(\(angle.degrees)deg)"]
}
}

View File

@ -24,4 +24,33 @@ let tokamakStyles = """
width: 0;
height: 0;
}
._tokamak-list {
list-style: none;
overflow-y: auto;
width: 100%;
height: 100%;
padding: 0;
}
._tokamak-disclosuregroup-label {
cursor: pointer;
}
._tokamak-disclosuregroup-chevron-container {
width: .25em;
height: .25em;
padding: 10px;
display: inline-block;
}
._tokamak-disclosuregroup-chevron {
width: 100%;
height: 100%;
transform: rotate(45deg);
border-right: solid 2px rgba(0, 0, 0, 0.25);
border-top: solid 2px rgba(0, 0, 0, 0.25);
}
._tokamak-disclosuregroup-content {
display: flex;
flex-direction: column;
margin-left: 1em;
}
"""

View File

@ -21,3 +21,9 @@ public typealias DefaultTextFieldStyle = TokamakCore.DefaultTextFieldStyle
public typealias PlainTextFieldStyle = TokamakCore.PlainTextFieldStyle
public typealias RoundedBorderTextFieldStyle = TokamakCore.RoundedBorderTextFieldStyle
public typealias SquareBorderTextFieldStyle = TokamakCore.SquareBorderTextFieldStyle
public typealias DefaultListStyle = TokamakCore.DefaultListStyle
public typealias PlainListStyle = TokamakCore.PlainListStyle
public typealias InsetListStyle = TokamakCore.InsetListStyle
public typealias GroupedListStyle = TokamakCore.GroupedListStyle
public typealias InsetGroupedListStyle = TokamakCore.InsetGroupedListStyle

View File

@ -0,0 +1,91 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/3/20.
//
import TokamakCore
public typealias DisclosureGroup = TokamakCore.DisclosureGroup
public typealias OutlineGroup = TokamakCore.OutlineGroup
extension DisclosureGroup: ViewDeferredToRenderer {
var chevron: some View {
HTML("div",
["class": "_tokamak-disclosuregroup-chevron-container"],
listeners: [
"click": { _ in
_DisclosureGroupProxy(self).toggleIsExpanded()
},
]) {
HTML("div", ["class": "_tokamak-disclosuregroup-chevron"])
.rotationEffect(_DisclosureGroupProxy(self).isExpanded ?
.degrees(90) :
.degrees(0))
}
}
var label: some View {
HTML("div", ["class": "_tokamak-disclosuregroup-label"]) { () -> AnyView in
switch _DisclosureGroupProxy(self).style {
case is _ListOutlineGroupStyle:
return AnyView(HStack {
_DisclosureGroupProxy(self).label
Spacer()
chevron
})
default:
return AnyView(HStack {
chevron
_DisclosureGroupProxy(self).label
})
}
}
}
var content: some View {
HTML("div", [
"class": "_tokamak-disclosuregroup-content",
"role": "treeitem",
"aria-expanded": _DisclosureGroupProxy(self).isExpanded ? "true" : "false",
]) { () -> AnyView in
if _DisclosureGroupProxy(self).isExpanded {
return AnyView(_DisclosureGroupProxy(self).content())
} else {
return AnyView(EmptyView())
}
}
}
public var deferredBody: AnyView {
AnyView(HTML("div", [
"class": "_tokamak-disclosuregroup",
"role": "tree",
]) { () -> AnyView in
switch _DisclosureGroupProxy(self).style {
case is _ListOutlineGroupStyle:
return AnyView(VStack(alignment: .leading) {
label
Divider()
content
})
default:
return AnyView(VStack(alignment: .leading) {
label
content
})
}
})
}
}

View File

@ -0,0 +1,35 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import TokamakCore
public typealias Divider = TokamakCore.Divider
extension Divider: AnyHTML {
var innerHTML: String? { nil }
var tag: String { "hr" }
var attributes: [String: String] {
[
"style": """
width: 100%; height: 0; margin: 0;
border-top: none;
border-right: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
border-left: none;
""",
]
}
var listeners: [String: Listener] { [:] }
}

View File

@ -0,0 +1,93 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import TokamakCore
public typealias List = TokamakCore.List
public typealias Section = TokamakCore.Section
extension PlainListStyle: ListStyleDeferredToRenderer {
public func sectionHeader<Header>(_ header: Header) -> AnyView where Header: View {
AnyView(header
.padding(.vertical, 5)
.background(Color(0xDDDDDD)))
}
public func sectionFooter<Footer>(_ footer: Footer) -> AnyView where Footer: View {
AnyView(VStack {
Divider()
_ListRow.listRow(footer, self, isLast: true)
})
}
}
extension GroupedListStyle: ListStyleDeferredToRenderer {
public func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View {
AnyView(content
.padding(.top, 20)
.background(Color(0xEEEEEE))
)
}
public func sectionHeader<Header>(_ header: Header) -> AnyView where Header: View {
AnyView(header
.font(.caption)
.padding(.leading, 20))
}
public func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View {
AnyView(section
.background(Color.white)
.padding(.top))
}
public func sectionFooter<Footer>(_ footer: Footer) -> AnyView where Footer: View {
AnyView(footer
.font(.caption)
.padding(.leading, 20))
}
}
extension InsetGroupedListStyle: ListStyleDeferredToRenderer {
public func listBody<ListBody>(_ content: ListBody) -> AnyView where ListBody: View {
AnyView(content
.padding(.top, 20)
.background(Color(0xEEEEEE))
)
}
public func listRow<Row>(_ row: Row) -> AnyView where Row: View {
AnyView(row
.padding([.leading, .trailing, .top, .bottom]))
}
public func sectionHeader<Header>(_ header: Header) -> AnyView where Header: View {
AnyView(header
.font(.caption)
.padding(.leading, 20))
}
public func sectionBody<SectionBody>(_ section: SectionBody) -> AnyView where SectionBody: View {
AnyView(section
.background(Color.white)
.cornerRadius(10)
.padding(.all))
}
public func sectionFooter<Footer>(_ footer: Footer) -> AnyView where Footer: View {
AnyView(footer
.font(.caption)
.padding(.leading, 20))
}
}

View File

@ -0,0 +1,54 @@
// Copyright 2019-2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/2/20.
//
#if canImport(SwiftUI)
import SwiftUI
#else
import TokamakDOM
#endif
public struct ListDemo: View {
let fs: [File] = [
.init(id: 0, name: "Users", children: [
.init(id: 1, name: "carson", children: [
.init(id: 2, name: "home", children: [
.init(id: 3, name: "Documents", children: nil),
.init(id: 4, name: "Desktop", children: nil),
]),
]),
]),
]
public var body: some View {
List {
ForEach(0..<3) {
Text("Outside Section: \($0 + 1)")
}
Section(header: Text("1-10"), footer: Text("End of section")) {
ForEach(0..<10) {
Text("Item: \($0 + 1)")
}
}
Section(header: Text("11-20")) {
ForEach(10..<20) {
Text("Item: \($0 + 1)")
}
}
}
.listStyle(InsetGroupedListStyle())
}
}

View File

@ -0,0 +1,46 @@
// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Carson Katri on 7/3/20.
//
import TokamakDOM
struct File: Identifiable {
let id: Int
let name: String
let children: [File]?
}
struct OutlineGroupDemo: View {
let fs: [File] = [
.init(id: 0, name: "Users", children: [
.init(id: 1, name: "carson", children: [
.init(id: 2, name: "home", children: [
.init(id: 3, name: "Documents", children: nil),
.init(id: 4, name: "Desktop", children: nil),
]),
]),
]),
]
var body: some View {
OutlineGroup(fs, children: \.children) { folder in
HStack {
Text(folder.children == nil ? "" : "🗂")
Text(folder.name)
}
}
}
}

View File

@ -49,10 +49,10 @@ struct TokamakDemoView: View {
#endif
TextFieldDemo()
SpacerDemo()
Spacer()
Text("Forced to bottom.")
EnvironmentDemo()
.font(.system(size: 8))
ListDemo()
OutlineGroupDemo()
}
}
}

View File

@ -83,7 +83,7 @@ Table columns:
| | | |
| --- | ------------------------------------------------------------------------------------------ | :-: |
| | [List](https://developer.apple.com/documentation/swiftui/list) | |
| 🚧 | [List](https://developer.apple.com/documentation/swiftui/list) | |
| 🚧 | [ForEach](https://developer.apple.com/documentation/swiftui/foreach) | |
| 🚧 | [ScrollView](https://developer.apple.com/documentation/swiftui/scrollview) | |
| | [ScrollViewReader](https://developer.apple.com/documentation/swiftui/scrollviewreader) | β |
@ -97,14 +97,14 @@ Table columns:
| | [Form](https://developer.apple.com/documentation/swiftui/form) | |
| ✅ | [Group](https://developer.apple.com/documentation/swiftui/group) | |
| | [GroupBox](https://developer.apple.com/documentation/swiftui/groupbox) | |
| | [Section](https://developer.apple.com/documentation/swiftui/section) | |
| 🚧 | [Section](https://developer.apple.com/documentation/swiftui/section) | |
### Hierarchical Views
| | | |
| --- | ------------------------------------------------------------------------------------ | :-: |
| | [OutlineGroup](https://developer.apple.com/documentation/swiftui/outlinegroup) | β |
| | [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) | β |
|🚧| [OutlineGroup](https://developer.apple.com/documentation/swiftui/outlinegroup) | β |
|🚧| [DisclosureGroup](https://developer.apple.com/documentation/swiftui/disclosuregroup) | β |
### Spacers and Dividers