Sources/OrgSwiftUI/OrgListView.swift
88 lines · 3201 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 VStack(alignment: .leading, spacing: 6) {
33 if let checkbox = item.checkbox {
34 HStack(alignment: .firstTextBaseline, spacing: 6) {
35 Image(systemName: Self.symbol(for: checkbox))
36 .foregroundStyle(checkbox == .on ? Color.accentColor : .secondary)
37 .accessibilityLabel(Self.label(for: checkbox))
38 paragraphs(item)
39 }
40 } else {
41 paragraphs(item)
42 }
43 if let sublist = item.sublist {
44 OrgListView(list: sublist)
45 }
46 }
47 }
48 }
49
50 /// `term :: definition` — the term above, the definition indented under it.
51 private func descriptionItem(_ item: OrgListLayout.Item) -> some View {
52 VStack(alignment: .leading, spacing: 2) {
53 if let term = item.term {
54 Text(term).fixedSize(horizontal: false, vertical: true)
55 }
56 paragraphs(item).padding(.leading, 16)
57 if let sublist = item.sublist {
58 OrgListView(list: sublist).padding(.leading, 16)
59 }
60 }
61 }
62
63 private func paragraphs(_ item: OrgListLayout.Item) -> some View {
64 VStack(alignment: .leading, spacing: 6) {
65 ForEach(Array(item.paragraphs.enumerated()), id: \.offset) { _, paragraph in
66 Text(paragraph)
67 .fixedSize(horizontal: false, vertical: true)
68 .frame(maxWidth: .infinity, alignment: .leading)
69 }
70 }
71 }
72
73 private static func symbol(for checkbox: OrgCheckbox) -> String {
74 switch checkbox {
75 case .on: "checkmark.square.fill"
76 case .off: "square"
77 case .partial: "minus.square"
78 }
79 }
80
81 private static func label(for checkbox: OrgCheckbox) -> String {
82 switch checkbox {
83 case .on: "Done"
84 case .off: "Not done"
85 case .partial: "Partly done"
86 }
87 }
88}