~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The iOS app's code view scrolls sideways rather than wrapping

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:ios/code-scroll
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 78
review.knotless.dev

The widest line is picked by Character count, but the width that matters is drawn width. A shorter line of Japanese or emoji can be wider than a longer ASCII line, even in a monospaced font, because those glyphs take two cells or more. Tabs have the same problem. The hidden sizer would then be too narrow. The LazyVStack would grow the column when the real widest row is first built, and the scroll extent would jump, which is what the sizer is meant to prevent. Your test line mixes ASCII with Japanese, so it may not have hit this. Consider weighting wide scalars, for example East Asian wide characters and emoji, as 2 when choosing the sizer.

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
//