Sources/OrgSwiftUI/OrgListView.swift
77 lines · 2790 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 @Environment(\.orgMath) private var math
11 /// Ordered numbering restarts at each nesting level, as in org.
12 var body: some View {
13 VStack(alignment: .leading, spacing: 6) {
14 ForEach(Array(list.items.enumerated()), id: \.element.id) { index, item in
15 if list.kind == .description {
16 descriptionItem(item)
17 } else {
18 markedItem(item, marker: marker(at: index))
19 }
20 }
21 }
22 }
23
24 private func marker(at index: Int) -> String {
25 list.kind == .ordered ? "\(index + 1)." : "•"
26 }
27
28 private func markedItem(_ item: OrgListLayout.Item, marker: String) -> some View {
29 HStack(alignment: .firstTextBaseline, spacing: 8) {
30 Text(marker)
31 .monospacedDigit()
32 .foregroundStyle(.secondary)
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 body(item)
39 }
40 } else {
41 body(item)
42 }
43 }
44 }
45
46 /// `term :: definition` — the term above, the definition indented under it.
47 private func descriptionItem(_ item: OrgListLayout.Item) -> some View {
48 VStack(alignment: .leading, spacing: 2) {
49 if let term = item.term {
50 orgText(term, math: math).fixedSize(horizontal: false, vertical: true)
51 }
52 body(item).padding(.leading, 16)
53 }
54 }
55
56 /// The item's blocks, rendered by the same view the document body uses — a nested list,
57 /// a source block, and a table all come out looking as they would anywhere else.
58 private func body(_ item: OrgListLayout.Item) -> some View {
59 OrgBlockListView(blocks: item.blocks)
60 }
61
62 private static func symbol(for checkbox: OrgCheckbox) -> String {
63 switch checkbox {
64 case .on: "checkmark.square.fill"
65 case .off: "square"
66 case .partial: "minus.square"
67 }
68 }
69
70 private static func label(for checkbox: OrgCheckbox) -> String {
71 switch checkbox {
72 case .on: "Done"
73 case .off: "Not done"
74 case .partial: "Partly done"
75 }
76 }
77}