| | |
| | | |
| | | private struct LayoutItem { |
| | | let index: Int |
| | | let frame: NSRect |
| | | var frame: NSRect |
| | | } |
| | | |
| | | private struct LayoutPlan { |
| | |
| | | let columnWidth = floor((availableWidth - gap * CGFloat(columnCount - 1)) / CGFloat(columnCount)) |
| | | var columnHeights = Array(repeating: outerPadding, count: columnCount) |
| | | var items: [LayoutItem] = [] |
| | | var lastItemIndexForColumn = Array<Int?>(repeating: nil, count: columnCount) |
| | | let bottomPinnedGroups = groups.enumerated() |
| | | .filter { bottomPinnedPriority(for: $0.element.containerID) != nil } |
| | | .sorted { |
| | | let lhsPriority = bottomPinnedPriority(for: $0.element.containerID) ?? Int.max |
| | | let rhsPriority = bottomPinnedPriority(for: $1.element.containerID) ?? Int.max |
| | | if lhsPriority != rhsPriority { return lhsPriority < rhsPriority } |
| | | return $0.offset < $1.offset |
| | | } |
| | | let bottomPinnedIndexes = Set(bottomPinnedGroups.map { $0.offset }) |
| | | |
| | | for (index, group) in groups.enumerated() { |
| | | if bottomPinnedIndexes.contains(index) { continue } |
| | | |
| | | let columnIndex = columnHeights.indices.min { columnHeights[$0] < columnHeights[$1] } ?? 0 |
| | | let x = outerPadding + CGFloat(columnIndex) * (columnWidth + gap) |
| | | let y = columnHeights[columnIndex] |
| | |
| | | frame: NSRect(x: x, y: y, width: columnWidth, height: height) |
| | | ) |
| | | ) |
| | | lastItemIndexForColumn[columnIndex] = items.count - 1 |
| | | columnHeights[columnIndex] = y + height + gap |
| | | } |
| | | |
| | | let tallest = columnHeights.max() ?? outerPadding |
| | | let contentHeight = (groups.isEmpty ? outerPadding * 2 : tallest - gap + outerPadding) |
| | | var contentBottom = columnHeights.max() ?? outerPadding |
| | | if !bottomPinnedGroups.isEmpty { |
| | | let alignedBottom = max(outerPadding, contentBottom - gap) |
| | | for itemIndex in lastItemIndexForColumn.compactMap({ $0 }) { |
| | | let currentFrame = items[itemIndex].frame |
| | | let alignedHeight = alignedBottom - currentFrame.minY |
| | | if alignedHeight > currentFrame.height { |
| | | items[itemIndex].frame.size.height = alignedHeight |
| | | } |
| | | } |
| | | } |
| | | for (index, group) in bottomPinnedGroups { |
| | | let y = max(outerPadding, contentBottom) |
| | | let height = AppGridCollectionMetrics.cardHeight( |
| | | appCount: group.apps.count, |
| | | width: availableWidth, |
| | | iconSize: iconSize |
| | | ) |
| | | items.append( |
| | | LayoutItem( |
| | | index: index, |
| | | frame: NSRect(x: outerPadding, y: y, width: availableWidth, height: height) |
| | | ) |
| | | ) |
| | | contentBottom = y + height + gap |
| | | } |
| | | |
| | | let contentHeight = (items.isEmpty ? outerPadding * 2 : contentBottom - gap + outerPadding) |
| | | + max(0, bottomContentPadding) |
| | | return LayoutPlan( |
| | | items: items, |
| | |
| | | ) |
| | | } |
| | | |
| | | private static func bottomPinnedPriority(for containerID: String) -> Int? { |
| | | switch containerID { |
| | | case AppContainerID.uncategorized: |
| | | return 0 |
| | | case AppContainerID.appleBuiltIn: |
| | | return 1 |
| | | default: |
| | | return nil |
| | | } |
| | | } |
| | | |
| | | private struct RowItem { |
| | | let index: Int |
| | | let width: CGFloat |