| 1 | import UIKit |
| 2 | |
| 3 | /// The page's paper from `sb_view_paper`, with the choices OneNote offers. |
| 4 | struct 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 | |
| 23 | extension 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. |
| 37 | final 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 | } |