Files
ios-mail/Modules/App/Sources/UI/Views/OneLineLabelsView/OneLineLabelsListView.swift
T

124 lines
4.2 KiB
Swift

// Copyright (c) 2024 Proton Technologies AG
//
// This file is part of Proton Mail.
//
// Proton Mail is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
import Foundation
import SwiftUI
import InboxDesignSystem
struct OneLineLabelsListView: View {
let labels: [LabelUIModel]
private let labelsSpacing: CGFloat = DS.Spacing.small
private let horizontalPadding: CGFloat = DS.Spacing.compact
private let minimalLabelWidth: CGFloat = DS.Spacing.extraLarge
private let height: CGFloat = 20
private let font = UIFont.caption2Semibold()
var body: some View {
GeometryReader { geometry in
HStack(spacing: labelsSpacing) {
ForEach(labelsFittingSpace(labels: labels, availableWidth: geometry.size.width), id: \.self) { label in
switch label {
case .regular(let viewModel):
Text(viewModel.text)
.font(.from(uiFont: .caption2Semibold()))
.foregroundColor(DS.Color.Global.white)
.lineLimit(1)
.frame(height: height)
.frame(minWidth: minimalLabelWidth)
.padding(.horizontal, horizontalPadding)
.background(viewModel.color)
.clipShape(RoundedRectangle(cornerRadius: DS.Radius.medium))
case .count(let count):
Text("+\(count)".notLocalized) // FIXME: - Reuse
.foregroundStyle(DS.Color.Text.weak)
.font(.caption)
}
}
}
}.frame(height: height)
}
// MARK: - Private
private enum Label: Hashable {
case regular(model: LabelUIModel)
case count(Int)
}
@MainActor
private func labelsFittingSpace(labels: [LabelUIModel], availableWidth: CGFloat) -> [Label] {
let padding = 2 * horizontalPadding
var availableWidth: CGFloat = availableWidth
var labelsToDisplay: [Label] = []
for label in labels {
let labelWidth = calculateLabel(label.text, font: font) + padding
let requiredSpace = labelWidth + labelsSpacing
if requiredSpace < availableWidth || labelsToDisplay.isEmpty {
availableWidth -= requiredSpace
labelsToDisplay.append(.regular(model: label))
} else {
labelsToDisplay.append(.count(labels.count - labelsToDisplay.count))
break
}
}
return labelsToDisplay
}
@MainActor
private func calculateLabel(_ text: String, font: UIFont) -> CGFloat {
let label = UILabel()
label.text = text
label.font = font
label.numberOfLines = 1
label.sizeToFit()
return max(label.frame.width, minimalLabelWidth)
}
}
private extension UIFont {
static func caption2Semibold() -> UIFont {
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .caption2)
let semiboldDescriptor = descriptor.addingAttributes([
UIFontDescriptor.AttributeName.traits: [
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold
]
])
return UIFont(descriptor: semiboldDescriptor, size: .zero)
}
}
private extension Font {
static func from(uiFont: UIFont) -> Font {
return Font(uiFont as CTFont)
}
}
#Preview {
VStack {
ForEach(OneLineLabelsListViewPreviewDataProvider.labels, id: \.self) { labels in
OneLineLabelsListView(labels: labels)
}
Spacer()
}.padding()
}