1import UIKit
2
3/// The page's paper from `sb_view_paper`, with the choices OneNote offers.
4struct Paper: Decodable {
5 struct Color: Decodable {
6 let name: String
7 let rgb: [UInt8]
8
9 init(from decoder: Decoder) throws {
10 var pair = try decoder.unkeyedContainer()
11 name = try pair.decode(String.self)
12 rgb = try pair.decode([UInt8].self)
13 }
14 }
15
16 let color: [UInt8]?
17 let ruled: Int?
18 let colors: [Color]
19 let rules: [String]
20 let templates: [String]
21}
22
23extension UIColor {
24 convenience init(rgb: [UInt8]) {
25 self.init(red: CGFloat(rgb[0]) / 255, green: CGFloat(rgb[1]) / 255, blue: CGFloat(rgb[2]) / 255, alpha: 1)
26 }
27
28 var rgb: [UInt8] {
29 var (red, green, blue, alpha): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
30 getRed(&red, green: &green, blue: &blue, alpha: &alpha)
31 return [red, green, blue].map { UInt8((min(1, max(0, $0)) * 255).rounded()) }
32 }
33}
34
35/// OneNote's View, Page Color and Rule Lines, and a template's art behind the page, as a
36/// sheet over the page, each choice taking effect at once.
37final class PaperViewController: UITableViewController {
38 private enum Part: Int, CaseIterable { case color, rules, background }
39
40 private let canvas: CanvasView
41 private var paper: Paper?
42 private let swatches = UIStackView()
43 private let well = UIColorWell()
44
45 init(canvas: CanvasView) {
46 self.canvas = canvas
47 super.init(style: .insetGrouped)
48 title = "Page Background"
49 }
50
51 required init?(coder: NSCoder) { fatalError() }
52
53 override func viewDidLoad() {
54 super.viewDidLoad()
55 tableView.register(UITableViewCell.self, forCellReuseIdentifier: "row")
56 navigationItem.rightBarButtonItem = UIBarButtonItem(
57 systemItem: .done, primaryAction: UIAction { [weak self] _ in self?.dismiss(animated: true) })
58 well.title = "Custom Color"
59 well.supportsAlpha = false
60 well.addAction(UIAction { [weak self] _ in
61 guard let self, let color = well.selectedColor else { return }
62 canvas.setPaper(color: color.rgb, ruled: paper?.ruled)
63 reload()
64 }, for: .valueChanged)
65 reload()
66 }
67
68 private func reload() {
69 paper = canvas.pagePaper
70 showSwatches()
71 tableView.reloadData()
72 }
73
74 private func showSwatches() {
75 swatches.arrangedSubviews.forEach { $0.removeFromSuperview() }
76 guard let paper else { return }
77 let none = swatch(nil, name: "No Color", chosen: paper.color == nil)
78 swatches.addArrangedSubview(none)
79 for color in paper.colors {
80 swatches.addArrangedSubview(swatch(color.rgb, name: color.name, chosen: paper.color == color.rgb))
81 }
82 let custom = paper.color.flatMap { color in paper.colors.contains { $0.rgb == color } ? nil : color }
83 well.selectedColor = custom.map(UIColor.init(rgb:))
84 swatches.addArrangedSubview(well)
85 }
86
87 private func swatch(_ rgb: [UInt8]?, name: String, chosen: Bool) -> UIButton {
88 var configuration = UIButton.Configuration.plain()
89 configuration.background.backgroundColor = rgb.map(UIColor.init(rgb:)) ?? .white
90 configuration.background.cornerRadius = 16
91 configuration.background.strokeColor = chosen ? .tintColor : .separator
92 configuration.background.strokeWidth = chosen ? 3 : 1
93 if rgb == nil {
94 configuration.image = UIImage(systemName: "line.diagonal")
95 configuration.baseForegroundColor = .systemRed
96 }
97 let button = UIButton(configuration: configuration, primaryAction: UIAction { [weak self] _ in
98 guard let self else { return }
99 canvas.setPaper(color: rgb, ruled: paper?.ruled)
100 reload()
101 })
102 button.accessibilityLabel = name
103 button.accessibilityTraits.insert(chosen ? .selected : [])
104 button.widthAnchor.constraint(equalToConstant: 32).isActive = true
105 button.heightAnchor.constraint(equalToConstant: 32).isActive = true
106 return button
107 }
108
109 private let parts = Part.allCases
110
111 override func numberOfSections(in tableView: UITableView) -> Int { parts.count }
112
113 override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
114 switch parts[section] {
115 case .color: "Page Color"
116 case .rules: "Rule Lines"
117 case .background: "Background"
118 }
119 }
120
121 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
122 guard let paper else { return 0 }
123 return switch parts[section] {
124 case .color: 1
125 case .rules: 1 + paper.rules.count
126 case .background: 1 + paper.templates.count
127 }
128 }
129
130 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
131 let cell = tableView.dequeueReusableCell(withIdentifier: "row", for: indexPath)
132 cell.contentView.subviews.filter { $0 is UIScrollView }.forEach { $0.removeFromSuperview() }
133 cell.accessoryType = .none
134 cell.contentConfiguration = nil
135 guard let paper else { return cell }
136 switch parts[indexPath.section] {
137 case .color:
138 cell.selectionStyle = .none
139 let scroll = UIScrollView()
140 scroll.showsHorizontalScrollIndicator = false
141 swatches.spacing = 12
142 swatches.alignment = .center
143 swatches.translatesAutoresizingMaskIntoConstraints = false
144 scroll.translatesAutoresizingMaskIntoConstraints = false
145 scroll.addSubview(swatches)
146 cell.contentView.addSubview(scroll)
147 NSLayoutConstraint.activate([
148 scroll.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor),
149 scroll.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor),
150 scroll.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
151 scroll.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
152 scroll.heightAnchor.constraint(equalToConstant: 56),
153 swatches.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor, constant: 16),
154 swatches.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor, constant: -16),
155 swatches.centerYAnchor.constraint(equalTo: scroll.frameLayoutGuide.centerYAnchor),
156 ])
157 case .rules:
158 cell.selectionStyle = .default
159 var content = cell.defaultContentConfiguration()
160 content.text = indexPath.row == 0 ? "None" : paper.rules[indexPath.row - 1]
161 cell.contentConfiguration = content
162 let chosen = indexPath.row == 0 ? paper.ruled == nil : paper.ruled == indexPath.row - 1
163 cell.accessoryType = chosen ? .checkmark : .none
164 case .background:
165 cell.selectionStyle = .default
166 var content = cell.defaultContentConfiguration()
167 content.text = indexPath.row == 0 ? "None" : paper.templates[indexPath.row - 1]
168 cell.contentConfiguration = content
169 }
170 return cell
171 }
172
173 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
174 tableView.deselectRow(at: indexPath, animated: true)
175 guard let paper else { return }
176 switch parts[indexPath.section] {
177 case .color: return
178 case .rules: canvas.setPaper(color: paper.color, ruled: indexPath.row == 0 ? nil : indexPath.row - 1)
179 case .background: canvas.setArt(indexPath.row == 0 ? nil : paper.templates[indexPath.row - 1])
180 }
181 reload()
182 }
183}