| | |
| | | private enum HorizontalMasonryMetrics { |
| | | static let horizontalPadding: CGFloat = 12 |
| | | static let verticalPadding: CGFloat = 0 |
| | | static let cardGap: CGFloat = 16 |
| | | static let horizontalGap: CGFloat = 16 |
| | | static let verticalGap: CGFloat = WaterfallMetrics.cardGap |
| | | static let cardMinWidth: CGFloat = 220 |
| | | static let cardMaxWidth: CGFloat = 320 |
| | | static let titleBarHeight: CGFloat = 26 |
| | | static let titleGap: CGFloat = 6 |
| | | static let minThumbnailHeight: CGFloat = 92 |
| | | static let maxThumbnailHeightFraction: CGFloat = 0.40 |
| | | static let cardHeight: CGFloat = WaterfallMetrics.cardHeight |
| | | } |
| | | |
| | | private enum WaterfallHorizontalIntent { |
| | |
| | | 1, |
| | | Int( |
| | | floor( |
| | | (availableWidth + HorizontalMasonryMetrics.cardGap) |
| | | / (HorizontalMasonryMetrics.cardMinWidth + HorizontalMasonryMetrics.cardGap) |
| | | (availableWidth + HorizontalMasonryMetrics.horizontalGap) |
| | | / (HorizontalMasonryMetrics.cardMinWidth + HorizontalMasonryMetrics.horizontalGap) |
| | | ) |
| | | ) |
| | | ) |
| | | let laneCount = min(maximumLaneCount, max(1, cards.count)) |
| | | let fittingWidth = ( |
| | | availableWidth - CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.cardGap |
| | | availableWidth - CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.horizontalGap |
| | | ) / CGFloat(laneCount) |
| | | let cardWidth = floor( |
| | | min( |
| | |
| | | ) |
| | | ) |
| | | let usedWidth = CGFloat(laneCount) * cardWidth |
| | | + CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.cardGap |
| | | + CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.horizontalGap |
| | | let startX = max( |
| | | HorizontalMasonryMetrics.horizontalPadding, |
| | | (viewportWidth - usedWidth) / 2 |
| | |
| | | } |
| | | return lhs.offset < rhs.offset |
| | | }?.offset ?? 0 |
| | | let cardHeight = horizontalMasonryCardHeight( |
| | | for: card, |
| | | width: cardWidth, |
| | | viewportHeight: waterfallLayer.bounds.height |
| | | ) |
| | | let x = startX + CGFloat(laneIndex) * (cardWidth + HorizontalMasonryMetrics.cardGap) |
| | | let cardHeight = HorizontalMasonryMetrics.cardHeight |
| | | let x = startX + CGFloat(laneIndex) * (cardWidth + HorizontalMasonryMetrics.horizontalGap) |
| | | let top = HorizontalMasonryMetrics.verticalPadding + laneHeights[laneIndex] |
| | | plannedFrames.append(( |
| | | windowID: card.item.window.id, |
| | |
| | | width: cardWidth, |
| | | height: cardHeight |
| | | )) |
| | | laneHeights[laneIndex] += cardHeight + HorizontalMasonryMetrics.cardGap |
| | | laneHeights[laneIndex] += cardHeight + HorizontalMasonryMetrics.verticalGap |
| | | } |
| | | |
| | | let tallestLane = laneHeights.max().map { |
| | | max(0, $0 - HorizontalMasonryMetrics.cardGap) |
| | | max(0, $0 - HorizontalMasonryMetrics.verticalGap) |
| | | } ?? 0 |
| | | let contentHeight = max( |
| | | waterfallLayer.bounds.height, |
| | |
| | | ) |
| | | |
| | | return HorizontalMasonryLayout(contentHeight: contentHeight, cardFramesByWindowID: frames) |
| | | } |
| | | |
| | | private func horizontalMasonryCardHeight( |
| | | for card: WaterfallCardLayers, |
| | | width: CGFloat, |
| | | viewportHeight: CGFloat |
| | | ) -> CGFloat { |
| | | let thumbnailHeight = min( |
| | | max( |
| | | HorizontalMasonryMetrics.minThumbnailHeight, |
| | | width / horizontalMasonryThumbnailAspectRatio(for: card) |
| | | ), |
| | | max(1, viewportHeight * HorizontalMasonryMetrics.maxThumbnailHeightFraction) |
| | | ) |
| | | return HorizontalMasonryMetrics.titleBarHeight |
| | | + HorizontalMasonryMetrics.titleGap |
| | | + thumbnailHeight |
| | | } |
| | | |
| | | private func horizontalMasonryThumbnailAspectRatio(for card: WaterfallCardLayers) -> CGFloat { |
| | | if card.item.window.isFullscreen { |
| | | return 16.0 / 9.0 |
| | | } |
| | | if card.item.window.isMinimized { |
| | | return 4.0 / 3.0 |
| | | } |
| | | |
| | | switch Int(card.item.window.id % 4) { |
| | | case 0: |
| | | return 16.0 / 9.0 |
| | | case 1: |
| | | return 3.0 / 2.0 |
| | | case 2: |
| | | return 4.0 / 3.0 |
| | | default: |
| | | return 5.0 / 4.0 |
| | | } |
| | | } |
| | | |
| | | private func horizontalMasonryContentFrame() -> CGRect { |