// // Copyright 2023, 2024 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. // @_exported import CompoundDesignTokens import SwiftUI public extension Image { /// The icons used by Element as defined in Compound Design Tokens. static let compound = CompoundIcons() } /// A view that displays an icon from Compound. The icon defaults to a size of 24pt /// and scales with Dynamic Type, relative to any font given to it by the `font` modifier. public struct CompoundIcon: View { /// The size of the icon. public enum Size { /// An icon size of 16pt. case xSmall /// An icon size of 20pt. case small /// An icon size of 24pt. case medium /// A custom icon size. case custom(CGFloat) var value: CGFloat { switch self { case .xSmall: return 16 case .small: return 20 case .medium: return 24 case .custom(let size): return size } } } private var image: Image private var size: Size private var font: Font private var fontSize: FontSize { FontSize.reflecting(font) ?? .style(.body) } /// Creates an icon using a key path from the Compound tokens. The size will be /// 24pt and will scale relative to the `bodyLG` font when Dynamic Type is used. /// /// - Parameters: /// - icon: The icon to show. public init(_ icon: KeyPath) { image = .compound[keyPath: icon] self.size = .medium self.font = .compound.bodyLG } /// Creates an icon using a key path from the Compound tokens. /// /// - Parameters: /// - icon: The icon to show. /// - size: The size of the icon. /// - font: The font that should be used for scaling with Dynamic Type. public init(_ icon: KeyPath, size: Size, relativeTo font: Font) { image = .compound[keyPath: icon] self.size = size self.font = font } /// Creates an icon using a custom image to allow assets from outside /// of Compound to scale in the same way as icons. The size will be 24pt /// and will scale relative to the `bodyLG` font when Dynamic Type is used. /// /// - Parameters: /// - customImage: The image that should be displayed /// /// ** Note:** The image should have a square frame or it may end up distorted. public init(customImage: Image) { image = customImage self.size = .medium self.font = .compound.bodyLG } /// Creates an icon using a custom image to allow assets from outside /// of Compound to scale in the same way as icons. /// /// - Parameters: /// - customImage: The image that should be displayed /// - size: The size of the icon. /// - font: The font that should be used for scaling with Dynamic Type. /// /// ** Note:** The image should have a square frame or it may end up distorted. public init(customImage: Image, size: Size, relativeTo font: Font) { image = customImage self.size = size self.font = font } public var body: some View { image .resizable() .modifier(CompoundIconFrame(fontSize: size.value, textStyle: fontSize.style)) } } /// A simple modifier that applies a square frame of a given size that will be /// scaled dynamically based upon the specified text style. private struct CompoundIconFrame: ViewModifier { @ScaledMetric private var size: CGFloat init(fontSize: CGFloat, textStyle: Font.TextStyle) { _size = ScaledMetric(wrappedValue: fontSize, relativeTo: textStyle) } func body(content: Content) -> some View { content .frame(width: size, height: size) } } public extension Label { /// Creates a label with an icon from Compound and a title generated from a string. /// The icon size will be 24pt, scaling relative to the `bodyLG` with Dynamic Type. /// - Parameters: /// - title: A string used as the label’s title. /// - icon: The icon to use from Compound. init(_ title: some StringProtocol, icon: KeyPath) where Title == Text, Icon == CompoundIcon { self.init { Text(title) } icon: { CompoundIcon(icon) } } /// Creates a label with an icon from Compound and a title generated from a string. /// - Parameters: /// - title: A string used as the label’s title. /// - icon: The icon to use from Compound. /// - iconSize: The size of the icon. /// - font: The font that the icon should scale relative to with Dynamic Type. init(_ title: some StringProtocol, icon: KeyPath, iconSize: CompoundIcon.Size, relativeTo font: Font) where Title == Text, Icon == CompoundIcon { self.init { Text(title) } icon: { CompoundIcon(icon, size: iconSize, relativeTo: font) } } } // MARK: - Previews struct CompoundIcon_Previews: PreviewProvider, TestablePreview { static var previews: some View { form .previewDisplayName("Form") buttons .padding(8) .previewLayout(.sizeThatFits) .previewDisplayName("Buttons") accessibilityIcons .previewDisplayName("Accessibility Icons Only") accessibilityLabels .previewDisplayName("Accessibility Labels") } static var accessibilityIcons: some View { VStack { ForEach(DynamicTypeSize.allCases, id: \.self) { size in HStack { CompoundIcon(\.userProfile, size: .xSmall, relativeTo: .compound.bodyXS) CompoundIcon(\.userProfile, size: .small, relativeTo: .compound.bodySM) CompoundIcon(\.userProfile, size: .medium, relativeTo: .compound.bodyLG) } .dynamicTypeSize(size) } } } static var accessibilityLabels: some View { Grid(alignment: .leading) { ForEach(DynamicTypeSize.allCases, id: \.self) { size in GridRow { Label("Test XS", icon: \.userProfile, iconSize: .xSmall, relativeTo: .compound.bodyXS) .font(.compound.bodyXS) Label("Test Small", icon: \.userProfile, iconSize: .small, relativeTo: .compound.bodySM) .font(.compound.bodySM) Label("Test Medium", icon: \.userProfile, iconSize: .medium, relativeTo: .compound.bodyLG) .font(.compound.bodyLG) } .lineLimit(1) .dynamicTypeSize(size) } } } static var form: some View { Form { Section { ListRow(label: .action(title: "Plain Icon", icon: \.userProfile), kind: .label) ListRow(label: .default(title: "Plain Icon", icon: \.userProfile), kind: .label) ListRow(label: .default(title: "Plain Icon", systemIcon: .personCropCircle), kind: .label) } } .compoundList() .safeAreaInset(edge: .bottom) { Button { } label: { Label("Button", icon: \.userProfile) } .buttonStyle(.compound(.primary)) .padding() } } static var buttons: some View { VStack { Button { } label: { Label { Text("Body Large") } icon: { CompoundIcon(\.userProfile, size: .medium, relativeTo: .compound.bodyLG) } } .font(.compound.bodyLG) .buttonStyle(.borderedProminent) Button { } label: { Label { Text("Body Small") } icon: { CompoundIcon(\.userProfile, size: .small, relativeTo: .compound.bodySM) } } .font(.compound.bodySM) .buttonStyle(.borderedProminent) Button { } label: { Label { Text("Body xSmall") } icon: { CompoundIcon(\.userProfile, size: .xSmall, relativeTo: .compound.bodyXS) } } .font(.compound.bodyXS) .buttonStyle(.borderedProminent) } } }