import Foundation /// Pure geometry helpers for the layout engine. enum LayoutGeometry { static let margin: CGFloat = 40 static let itemSpacing: CGFloat = 16 static let titleHeight: CGFloat = 36 static let minThumbnailWidth: CGFloat = 120 /// Find the optimal column count and thumbnail width for a flat grid of windows. /// Tries each column count and picks the one that produces the largest thumbnails /// while fitting everything on screen. static func optimalGrid( windows: [WindowInfo], availableWidth: CGFloat, availableHeight: CGFloat ) -> (width: CGFloat, columns: Int) { let count = windows.count guard count > 0 else { return (200, 1) } let maxCols = max(1, Int((availableWidth + itemSpacing) / (minThumbnailWidth + itemSpacing))) var bestWidth: CGFloat = 0 var bestCols = 1 for cols in 1...maxCols { let thumbWidth = (availableWidth - CGFloat(cols - 1) * itemSpacing) / CGFloat(cols) guard thumbWidth >= minThumbnailWidth else { continue } let needed = totalHeight(windows: windows, thumbWidth: thumbWidth, cols: cols) if needed <= availableHeight && thumbWidth > bestWidth { bestWidth = thumbWidth bestCols = cols } } // Binary search fallback if nothing fit if bestWidth == 0 { var lo: CGFloat = 20 var hi = availableWidth for _ in 0..<40 { let mid = (lo + hi) / 2 let cols = max(1, Int((availableWidth + itemSpacing) / (mid + itemSpacing))) let needed = totalHeight(windows: windows, thumbWidth: mid, cols: cols) if needed <= availableHeight { bestWidth = mid bestCols = cols lo = mid } else { hi = mid } } if bestWidth == 0 { bestCols = max(1, Int(ceil(sqrt(Double(count))))) bestWidth = max(20, (availableWidth - CGFloat(bestCols - 1) * itemSpacing) / CGFloat(bestCols)) } } return (bestWidth, bestCols) } /// Compute total height for a flat grid layout. static func totalHeight(windows: [WindowInfo], thumbWidth: CGFloat, cols: Int) -> CGFloat { let rowCount = Int(ceil(Double(windows.count) / Double(cols))) var height: CGFloat = 0 for row in 0.. 0 ? w.frame.height / w.frame.width : 0.625 let cellH = thumbWidth * ratio + titleHeight maxRowH = max(maxRowH, cellH) } height += maxRowH if row < rowCount - 1 { height += itemSpacing } } return height } /// Fit a source rect into a target rect maintaining aspect ratio. static func aspectFit(source: CGSize, target: CGSize) -> CGSize { guard source.width > 0, source.height > 0 else { return target } let scale = min(target.width / source.width, target.height / source.height) return CGSize(width: source.width * scale, height: source.height * scale) } }