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:
parent
aedab8ca05
commit
50be7b16f7
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
// }
|
|
@ -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)"]
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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] { [:] }
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,10 +49,10 @@ struct TokamakDemoView: View {
|
|||
#endif
|
||||
TextFieldDemo()
|
||||
SpacerDemo()
|
||||
Spacer()
|
||||
Text("Forced to bottom.")
|
||||
EnvironmentDemo()
|
||||
.font(.system(size: 8))
|
||||
ListDemo()
|
||||
OutlineGroupDemo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue