Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,31 @@ public nonisolated enum ColumnStripGeometry {
minimumWidths: [CGFloat] = []
) -> (frames: [CGRect], contentWidth: CGFloat) {
var x = gap
// Where the columns end before rounding. Only rounding is cut back: a column that runs
// past the trailing edge on its own fraction keeps its width, and the strip scrolls.
var idealX = gap
// A column widened to its minimum moves every later one off its unrounded place; from
// there on nothing is cut back or snapped, and the strip scrolls.
var widened = false
let trailingEdge = viewport.width - gap
var frames: [CGRect] = []
frames.reserveCapacity(widths.count)
let widest = max(1, viewport.width - gap * 2)
for (index, fraction) in widths.enumerated() {
var width = SplitGeometry.roundToPixel(pixelWidth(fraction: fraction, viewportWidth: viewport.width, gap: gap), scale: scale)
let room = viewport.width - gap - x
if width > room { width = max(1, room) }
let ideal = pixelWidth(fraction: fraction, viewportWidth: viewport.width, gap: gap)
var width = SplitGeometry.roundToPixel(ideal, scale: scale)
let idealEnd = idealX + ideal
idealX = idealEnd + gap
let room = trailingEdge - x
if !widened, room >= 1, idealEnd <= trailingEdge + edgeTolerance,
width > room || abs(idealEnd - trailingEdge) <= edgeTolerance {
// Fits before rounding: rounding neither pushes it past the edge nor stops it
// short of an edge it was meant to reach.
width = room
}
if index < minimumWidths.count, width < minimumWidths[index] {
width = max(width, SplitGeometry.ceilToPixel(min(minimumWidths[index], widest), scale: scale))
let minimum = SplitGeometry.ceilToPixel(min(minimumWidths[index], widest), scale: scale)
if minimum > width { width = minimum; widened = true }
}
frames.append(CGRect(x: x, y: 0, width: width, height: viewport.height))
x += width + gap
Expand All @@ -52,6 +68,9 @@ public nonisolated enum ColumnStripGeometry {
return (frames, contentWidth)
}

/// Float slack when a column's unrounded end is compared with the strip's trailing edge.
static let edgeTolerance: CGFloat = 0.01

public static func maxOffset(contentWidth: CGFloat, viewportWidth: CGFloat) -> CGFloat {
max(0, contentWidth - viewportWidth)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ import Testing
}
}

/// Only rounding is cut back: a column whose own fraction runs past the trailing edge keeps
/// its width, and the strip scrolls to it.
@Test(arguments: [1.0, 2.0])
func aColumnPastTheEdgeKeepsItsWidth(scale: CGFloat) {
let gap: CGFloat = 8
for viewport in Self.viewports {
let result = ColumnStripGeometry.frames(widths: [1, 1], viewport: CGSize(width: viewport, height: 800),
gap: gap, scale: scale)
let first = result.frames[0], second = result.frames[1]
#expect(abs(second.width - first.width) <= 1, "the second full column is as wide as the first in \(viewport)")
#expect(ColumnStripGeometry.maxOffset(contentWidth: result.contentWidth, viewportWidth: viewport) > viewport / 2)
}
}

/// The same through the screen geometry the layout view scrolls: a screen of equal columns
/// has no content past its viewport.
@Test(arguments: [2, 3, 4, 5])
Expand All @@ -42,8 +56,16 @@ import Testing
let columns = (0..<count).map { index in
LayoutColumn(id: ColumnID("c\(index)"), width: 1.0 / Double(count), root: .leaf(PaneID("p\(index)")))
}
let minimum = columns.map { $0.minimumSize(style: style).width }.max() ?? 0
for viewport in Self.viewports {
let geometry = ScreenGeometry.compute(.columns(columns), viewport: CGSize(width: viewport, height: 800), style: style, scale: 2)
let share = ColumnStripGeometry.pixelWidth(fraction: 1.0 / Double(count), viewportWidth: viewport, gap: style.stripGap)
guard share >= minimum else {
// Columns narrower than a pane's minimum widen to it (panes never collapse), so a
// strip too narrow for them scrolls: that overflow is asked for.
#expect(geometry.maxOffset > 0, "\(count) columns below their minimum in \(viewport) pt scroll")
continue
}
#expect(abs(geometry.contentWidth - viewport) < 0.001,
"\(count) columns in \(viewport) pt: content \(geometry.contentWidth)")
#expect(geometry.maxOffset < 0.001, "no horizontal scroll with \(count) columns in \(viewport) pt: \(geometry.maxOffset)")
Expand Down
Loading