Sources/OrgSwiftUI/OrgListView.swift
76 lines · 2730 bytes
1import OrgSwift
2import SwiftUI
3
4/// A list, recursing into its own sublists.
5///
6/// SwiftUI has no list-layout primitive, so the marker sits in its own column and the content
7/// hangs beside it — wrapped lines line up under the text, not under the bullet.
8struct OrgListView: View {
9 let list: OrgListLayout
10 /// Ordered numbering restarts at each nesting level, as in org.
11 var body: some View {
12 VStack(alignment: .leading, spacing: 6) {
13 ForEach(Array(list.items.enumerated()), id: \.element.id) { index, item in
14 if list.kind == .description {
15 descriptionItem(item)
16 } else {
17 markedItem(item, marker: marker(at: index))
18 }
19 }
20 }
21 }
22
23 private func marker(at index: Int) -> String {
24 list.kind == .ordered ? "\(index + 1)." : "•"
25 }
26
27 private func markedItem(_ item: OrgListLayout.Item, marker: String) -> some View {
28 HStack(alignment: .firstTextBaseline, spacing: 8) {
29 Text(marker)
30 .monospacedDigit()
31 .foregroundStyle(.secondary)
32 if let checkbox = item.checkbox {
33 HStack(alignment: .firstTextBaseline, spacing: 6) {
34 Image(systemName: Self.symbol(for: checkbox))
35 .foregroundStyle(checkbox == .on ? Color.accentColor : .secondary)
36 .accessibilityLabel(Self.label(for: checkbox))
37 body(item)
38 }
39 } else {
40 body(item)
41 }
42 }
43 }
44
45 /// `term :: definition` — the term above, the definition indented under it.
46 private func descriptionItem(_ item: OrgListLayout.Item) -> some View {
47 VStack(alignment: .leading, spacing: 2) {
48 if let term = item.term {
49 Text(term).fixedSize(horizontal: false, vertical: true)
50 }
51 body(item).padding(.leading, 16)
52 }
53 }
54
55 /// The item's blocks, rendered by the same view the document body uses — a nested list,
56 /// a source block, and a table all come out looking as they would anywhere else.
57 private func body(_ item: OrgListLayout.Item) -> some View {
58 OrgBlockListView(blocks: item.blocks)
59 }
60
61 private static func symbol(for checkbox: OrgCheckbox) -> String {
62 switch checkbox {
63 case .on: "checkmark.square.fill"
64 case .off: "square"
65 case .partial: "minus.square"
66 }
67 }
68
69 private static func label(for checkbox: OrgCheckbox) -> String {
70 switch checkbox {
71 case .on: "Done"
72 case .off: "Not done"
73 case .partial: "Partly done"
74 }
75 }
76}