chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The iOS app's code view scrolls sideways rather than wrapping
ios/Knotless/Views/CodeViews.swift+44−14
| @@ -48,20 +48,7 @@ struct FileView: View { | ||
}) { file in | ||
if let text = file.text { | ||
let lines = text.hasSuffix("\n") ? String(text.dropLast()).components(separatedBy: "\n") : text.components(separatedBy: "\n") | ||
let painted = Highlight.paint(lines: lines, ranges: file.ranges) | ||
ScrollView { | ||
LazyVStack(alignment: .leading, spacing: 1) { | ||
ForEach(painted.indices, id: \.self) { i in | ||
HStack(alignment: .firstTextBaseline, spacing: 8) { | ||
Text("\(i + 1)").frame(width: 38, alignment: .trailing).foregroundStyle(.secondary) | ||
Text(painted[i]).frame(maxWidth: .infinity, alignment: .leading) | ||
} | ||
.font(.caption.monospaced()) | ||
} | ||
} | ||
.padding(.vertical, 8) | ||
.padding(.trailing, 8) | ||
} | ||
CodeLines(lines: lines, painted: Highlight.paint(lines: lines, ranges: file.ranges)) | ||
} else { | ||
ContentUnavailableView("This file can't be shown here", systemImage: "doc") | ||
} | ||
| @@ -70,3 +57,46 @@ struct FileView: View { | ||
.navigationBarTitleDisplayMode(.inline) | ||
} | ||
} | ||
| ||
/// A file's lines, each on one row whatever its length. The code scrolls sideways | ||
/// under numbers that stay put. | ||
private struct CodeLines: View { | ||
let lines: [String] | ||
let painted: [AttributedString] | ||
| ||
/// Every row the same height, so the two columns line up whatever a line holds. | ||
@ScaledMetric(relativeTo: .caption) private var rowHeight = 17 | ||
| ||
var body: some View { | ||
ScrollView { | ||
HStack(alignment: .top, spacing: 8) { | ||
column(widest: "\(painted.count)", alignment: .trailing) { i in | ||
Text("\(i + 1)").foregroundStyle(.secondary) | ||
} | ||
.padding(.leading, 8) | ||
ScrollView(.horizontal) { | ||
column(widest: lines.max { $0.count < $1.count } ?? "", alignment: .leading) { i in | ||
ios/Knotless/Views/CodeViews.swiftline 78The widest line is picked by | ||
Text(painted[i]) | ||
} | ||
.padding(.trailing, 16) | ||
} | ||
} | ||
.padding(.vertical, 8) | ||
} | ||
} | ||
| ||
/// Rows made as they come into view. The widest, drawn with no height, holds | ||
/// the column at its full width from the start, so it doesn't jump on scrolling. | ||
private func column(widest: String, alignment: HorizontalAlignment, @ViewBuilder row: @escaping (Int) -> Text) -> some View { | ||
VStack(alignment: alignment, spacing: 0) { | ||
Text(widest).frame(height: 0).hidden().accessibilityHidden(true) | ||
LazyVStack(alignment: alignment, spacing: 0) { | ||
ForEach(painted.indices, id: \.self) { i in | ||
row(i).lineLimit(1).fixedSize().frame(height: rowHeight) | ||
} | ||
} | ||
} | ||
.font(.caption.monospaced()) | ||
.fixedSize(horizontal: true, vertical: false) | ||
} | ||
} | ||
ios/KnotlessUITests/LookTests.swift+2−0
| @@ -88,5 +88,7 @@ final class LookTests: XCTestCase { | ||
} | ||
if step.swipe == "up" { app.swipeUp() } | ||
if step.swipe == "down" { app.swipeDown() } | ||
if step.swipe == "left" { app.swipeLeft() } | ||
if step.swipe == "right" { app.swipeRight() } | ||
} | ||
} | ||
ios/look.mjs+1−1
| @@ -17,7 +17,7 @@ | ||
// --tap <label> tap what has that label or accessibility | ||
// identifier, scrolling to it, in turn | ||
// --type <text> type into the first text box, in turn | ||
// --swipe <up|down> swipe, in turn | ||
// --swipe <up|down|left|right> swipe, in turn | ||
// --top only the screen as it opens, not all of it | ||
// --light --dark only that; both if neither is given | ||
// | ||