chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The iOS app's diff keeps each line on one row, scrolling sideways
ios/Knotless/Views/FilesView.swift+61−29
| @@ -73,11 +73,10 @@ struct FileSection: View { | ||
Section { | ||
if open { | ||
if let lines { | ||
// One view, not one per row: a lazy stack re-estimating a | ||
// file's rows as they arrive leaves a gap above them. | ||
VStack(spacing: 0) { ForEach(lines) { DiffLine(line: $0) } } | ||
if lines.isEmpty { | ||
Text(note ?? "No changes to show.").font(.footnote).foregroundStyle(.secondary).padding() | ||
} else { | ||
DiffLines(lines: lines) | ||
} | ||
} else { | ||
ProgressView().frame(maxWidth: .infinity).padding().task { await load() } | ||
| @@ -138,11 +137,61 @@ struct FileSection: View { | ||
} | ||
} | ||
| ||
private struct DiffLine: View { | ||
let line: FileSection.Line | ||
/// A file's diff, each line on one row whatever its length, its indent kept. The | ||
/// code scrolls sideways under numbers that stay put, as a file's lines do. | ||
/// | ||
/// Whole columns, not lazy ones: a lazy stack re-estimating a file's rows as they | ||
/// arrive leaves a gap above them. | ||
private struct DiffLines: View { | ||
let lines: [FileSection.Line] | ||
| ||
private var tint: Color { | ||
switch line.row.kind { | ||
/// Every row a set height, so the two columns line up whatever a line holds. | ||
@ScaledMetric(relativeTo: .caption) private var codeHeight = 20 | ||
@ScaledMetric(relativeTo: .caption) private var hunkHeight = 28 | ||
@State private var width: CGFloat = 0 | ||
| ||
var body: some View { | ||
HStack(alignment: .top, spacing: 0) { | ||
VStack(spacing: 0) { | ||
ForEach(lines) { line in | ||
HStack(spacing: Space.s1_5) { | ||
Text(number(line.row)).frame(width: 34, alignment: .trailing).foregroundStyle(.secondary) | ||
Text(marker(line.row)).foregroundStyle(line.row.kind == "a" ? Color.success : Color.danger) | ||
.accessibilityHidden(true) | ||
} | ||
.padding(.horizontal, Space.s1_5) | ||
.frame(height: height(line.row)) | ||
.background(tint(line.row)) | ||
} | ||
} | ||
.fixedSize(horizontal: true, vertical: false) | ||
ScrollView(.horizontal) { | ||
VStack(alignment: .leading, spacing: 0) { | ||
ForEach(lines) { line in | ||
Text(line.text) | ||
.foregroundStyle(line.row.line ? Color.primary : Color.accent) | ||
.lineLimit(1) | ||
.fixedSize() | ||
.padding(.trailing, Space.s4) | ||
// The tint reaches the screen's edge on a short | ||
// line, and the widest line's end on a long one. | ||
.frame(minWidth: width, maxWidth: .infinity, minHeight: height(line.row), maxHeight: height(line.row), alignment: .leading) | ||
.background(tint(line.row)) | ||
} | ||
} | ||
.fixedSize(horizontal: true, vertical: false) | ||
} | ||
.onGeometryChange(for: CGFloat.self, of: \.size.width) { width = $0 } | ||
} | ||
.font(.caption.monospaced()) | ||
} | ||
| ||
private func height(_ row: DiffRow) -> CGFloat { | ||
row.line ? codeHeight : hunkHeight | ||
} | ||
| ||
private func tint(_ row: DiffRow) -> Color { | ||
switch row.kind { | ||
case "a": Color.success.opacity(0.14) | ||
case "d": Color.danger.opacity(0.12) | ||
case "h": Color.accent.opacity(0.12) | ||
| @@ -150,33 +199,16 @@ private struct DiffLine: View { | ||
} | ||
} | ||
| ||
private var marker: String { | ||
switch line.row.kind { | ||
private func marker(_ row: DiffRow) -> String { | ||
switch row.kind { | ||
case "a": "+" | ||
case "d": "-" | ||
default: " " | ||
} | ||
} | ||
| ||
var body: some View { | ||
HStack(alignment: .firstTextBaseline, spacing: Space.s1_5) { | ||
if line.row.line { | ||
Text(number).frame(width: 34, alignment: .trailing).foregroundStyle(.secondary) | ||
Text(marker).foregroundStyle(line.row.kind == "a" ? Color.success : Color.danger) | ||
.accessibilityHidden(true) | ||
} | ||
Text(line.text) | ||
.foregroundStyle(line.row.line ? Color.primary : Color.accent) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.padding(.leading, line.row.line ? 0 : 12) | ||
} | ||
.font(.caption.monospaced()) | ||
.padding(.horizontal, Space.s1_5) | ||
.padding(.vertical, line.row.line ? Space.s0_5 : Space.s1_5) | ||
.background(tint) | ||
} | ||
| ||
private var number: String { | ||
(line.row.kind == "d" ? line.row.old : line.row.new ?? line.row.old).map(String.init) ?? "" | ||
private func number(_ row: DiffRow) -> String { | ||
guard row.line else { return "" } | ||
return (row.kind == "d" ? row.old : row.new ?? row.old).map(String.init) ?? "" | ||
} | ||
} | ||