import CoreGraphics import Testing @testable import supacode struct CanvasTileLayoutTests { private let titleBarHeight: CGFloat = 28 private let spacing: CGFloat = 20 private var tiler: CanvasTileLayout { CanvasTileLayout(spacing: spacing, titleBarHeight: titleBarHeight) } private func keys(_ count: Int) -> [String] { (0.. [String: CanvasCardLayout] { tiler.layout(keys: keys(count), viewport: viewport, comfortableSize: comfortable) } /// Visual rect (terminal + title bar) of a laid-out card, used for overlap checks. private func visualRect(_ layout: CanvasCardLayout) -> CGRect { let width = layout.size.width let height = layout.size.height + titleBarHeight return CGRect( x: layout.position.x - width / 2, y: layout.position.y - height / 2, width: width, height: height ) } private func assertNoOverlap(_ rects: [CGRect], sourceLocation: SourceLocation = #_sourceLocation) { for outer in 0.. topRow[0].position.y) // 3-card row cards are narrower than 2-card row cards. #expect(bottomRow[0].size.width < topRow[0].size.width) } // MARK: - Fill & non-overlap @Test func cardsNeverOverlap() throws { // A realistic comfortable size triggers zoom > 1 for the denser counts; // overlap-freedom must hold at every zoom (a uniform frame scale). let comfortable = CGSize(width: 500, height: 340) let viewports = [CGSize(width: 1600, height: 900), CGSize(width: 900, height: 1600)] for viewport in viewports { for count in 1...12 { let layouts = layout(count, viewport: viewport, comfortable: comfortable) #expect(layouts.count == count) assertNoOverlap(layouts.values.map(visualRect)) } } } @Test func eachRowFillsViewportWidth() throws { let width: CGFloat = 1600 let layouts = layout(3, viewport: CGSize(width: width, height: 900)) let rects = (0..<3).compactMap { layouts["card\($0)"] }.map(visualRect) let minX = rects.map(\.minX).min()! let maxX = rects.map(\.maxX).max()! // Row spans from the left spacing to the right spacing of the viewport. #expect(abs(minX - spacing) < 0.001) #expect(abs(maxX - (width - spacing)) < 0.001) } // MARK: - Adaptive zoom @Test func fewComfortableCardsStayAtNativeZoom() throws { // Two cards on a wide viewport are already larger than `comfortable`, so the // frame equals the viewport (zoom == 1 → fitToView scale ≈ 1, native text). let viewport = CGSize(width: 1600, height: 900) let layouts = layout(2, viewport: viewport, comfortable: CGSize(width: 500, height: 340)) let maxX = layouts.values.map { visualRect($0).maxX }.max()! #expect(abs(maxX - (viewport.width - spacing)) < 0.001) } @Test func manySmallCardsEnlargeFrameForAdaptiveZoom() throws { // Twelve cards shrink each cell below `comfortable`, so the frame grows past // the viewport; fitToView then zooms out (smaller text, more terminal content). let viewport = CGSize(width: 1600, height: 900) let comfortable = CGSize(width: 500, height: 340) let adaptive = layout(12, viewport: viewport, comfortable: comfortable) let native = layout(12, viewport: viewport) // comfortable 1×1 → zoom 1 let adaptiveFrameWidth = adaptive.values.map { visualRect($0).maxX }.max()! + spacing let nativeFrameWidth = native.values.map { visualRect($0).maxX }.max()! + spacing // Adaptive frame is larger than the viewport, and larger than the un-zoomed // layout — i.e. every card surface gains resolution. #expect(adaptiveFrameWidth > viewport.width) #expect(adaptiveFrameWidth > nativeFrameWidth) let adaptiveMinWidth = adaptive.values.map(\.size.width).min()! let nativeMinWidth = native.values.map(\.size.width).min()! #expect(adaptiveMinWidth > nativeMinWidth) } // MARK: - Small viewports @Test func smallViewportTilesExactlyWithoutClamping() throws { // With no comfortable floor (1×1), tile sizes cards by dividing the viewport, // so a small viewport yields small cards rather than overlapping ones. let layouts = layout(4, viewport: CGSize(width: 400, height: 300)) let rects = (0..<4).compactMap { layouts["card\($0)"] }.map(visualRect) // 2×2 grid: each cell is well under the 300pt default minimum width. #expect(rects.allSatisfy { $0.width < 300 }) assertNoOverlap(rects) } }